onnx_runtime_ep_api/kernel.rs
1//! The [`Kernel`] trait and kernel-match / cost types (§4.2).
2
3use std::borrow::Cow;
4
5use onnx_runtime_ir::TensorLayout;
6
7use crate::error::Result;
8use crate::tensor::{TensorMut, TensorView};
9use crate::weight::WeightHandle;
10
11/// A cost estimate for running a kernel, consumed by the placement cost model
12/// (`docs/ORT2.md` §6). All time fields are in **microseconds**; a fuller model
13/// (roofline, calibration) lands in `onnx-runtime-cost-model` (Phase 2).
14///
15/// The struct is `#[non_exhaustive]`: the Phase-2 cost model may add fields
16/// (e.g. energy, occupancy) without breaking EP crates. Construct it via
17/// [`Cost::ZERO`], [`Cost::new`], or the `with_*` builders rather than a struct
18/// literal so those additions stay source-compatible.
19///
20/// The three time components (`compute_us`, `memory_us`, `transfer_us`) map onto
21/// the design's *compute*, *memory-traffic*, and *layout/transfer* estimates;
22/// `launch_us` captures fixed dispatch latency (§6.2 `launch_overhead`) and
23/// `bytes_moved` carries the raw memory-traffic figure a roofline model needs
24/// (§6.3, mirroring the design `Cost::memory_bytes`).
25#[derive(Clone, Copy, Debug, Default, PartialEq)]
26#[non_exhaustive]
27pub struct Cost {
28 /// Estimated compute time (µs).
29 pub compute_us: f64,
30 /// Estimated memory-traffic time (µs).
31 pub memory_us: f64,
32 /// Estimated layout-conversion / cross-device copy time at boundaries (µs).
33 pub transfer_us: f64,
34 /// Fixed kernel-launch / dispatch latency (µs), independent of size.
35 pub launch_us: f64,
36 /// Estimated bytes of memory traffic (for roofline / bandwidth models).
37 pub bytes_moved: u64,
38}
39
40impl Cost {
41 /// The zero cost (free op).
42 pub const ZERO: Cost = Cost {
43 compute_us: 0.0,
44 memory_us: 0.0,
45 transfer_us: 0.0,
46 launch_us: 0.0,
47 bytes_moved: 0,
48 };
49
50 /// A cost from its three time components; `launch_us` and `bytes_moved`
51 /// default to zero (set them via the builders).
52 pub fn new(compute_us: f64, memory_us: f64, transfer_us: f64) -> Self {
53 Self {
54 compute_us,
55 memory_us,
56 transfer_us,
57 ..Self::ZERO
58 }
59 }
60
61 /// Set the fixed launch/dispatch latency.
62 pub fn with_launch_us(mut self, launch_us: f64) -> Self {
63 self.launch_us = launch_us;
64 self
65 }
66
67 /// Set the estimated memory-traffic volume.
68 pub fn with_bytes_moved(mut self, bytes_moved: u64) -> Self {
69 self.bytes_moved = bytes_moved;
70 self
71 }
72
73 /// Total estimated wall time (µs): the sum of all time components.
74 pub fn total_us(&self) -> f64 {
75 self.compute_us + self.memory_us + self.transfer_us + self.launch_us
76 }
77}
78
79/// Result of [`crate::ExecutionProvider::supports_op`].
80///
81/// A decline reason travels with the decision that produced it. EPs must not
82/// maintain a separate reason table: colocating the reason with `Unsupported`
83/// keeps diagnostics from drifting away from the actual claim predicate.
84pub enum KernelMatch {
85 Supported {
86 cost: Cost,
87 /// Layouts the kernel requires for each input, if constrained.
88 required_input_layouts: Option<Vec<TensorLayout>>,
89 /// Layouts the kernel produces for each output.
90 output_layouts: Vec<TensorLayout>,
91 },
92 Unsupported {
93 /// Actionable explanation of what the EP accepts or how to fix fallback.
94 reason: Cow<'static, str>,
95 },
96}
97
98impl KernelMatch {
99 /// Construct an unsupported match with its actionable decline reason.
100 pub fn unsupported(reason: impl Into<Cow<'static, str>>) -> Self {
101 Self::Unsupported {
102 reason: reason.into(),
103 }
104 }
105
106 /// Whether the op is supported.
107 pub fn is_supported(&self) -> bool {
108 matches!(self, KernelMatch::Supported { .. })
109 }
110
111 /// The EP's decline reason, or `None` when the op is supported.
112 pub fn reason(&self) -> Option<&str> {
113 match self {
114 Self::Supported { .. } => None,
115 Self::Unsupported { reason } => Some(reason),
116 }
117 }
118}
119
120/// Whether a compiled kernel can participate in device-graph capture.
121///
122/// As with [`KernelMatch`], a decline reason travels with the decision that
123/// produced it. EPs must not maintain a separate reason table: capture
124/// eligibility is often shape-, dtype-, warmup-, or implementation-dependent,
125/// so separating the reason from the predicate would let diagnostics drift.
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub enum CaptureSupport {
128 /// The kernel's warmed execution path satisfies the device-graph contract.
129 Supported,
130 /// The kernel cannot currently be captured.
131 Unsupported {
132 /// Actionable explanation of the failed capture precondition.
133 reason: Cow<'static, str>,
134 },
135}
136
137impl CaptureSupport {
138 /// Construct an unsupported result with its actionable decline reason.
139 pub fn unsupported(reason: impl Into<Cow<'static, str>>) -> Self {
140 Self::Unsupported {
141 reason: reason.into(),
142 }
143 }
144
145 /// Whether the kernel can currently participate in device-graph capture.
146 pub fn is_supported(&self) -> bool {
147 matches!(self, Self::Supported)
148 }
149
150 /// The kernel's capture-decline reason, or `None` when capture is supported.
151 pub fn reason(&self) -> Option<&str> {
152 match self {
153 Self::Supported => None,
154 Self::Unsupported { reason } => Some(reason),
155 }
156 }
157}
158
159/// The concrete implementation selected by a kernel's internal dispatcher.
160///
161/// Unlike [`KernelMatch`] (the EP's claim over a node) and [`CaptureSupport`]
162/// (graph-capture eligibility), this records **which** implementation ran for an
163/// already-claimed node and **why** the dispatch predicate chose it. A single
164/// claimed op (e.g. `MatMulNBits`) can pick materially different kernels for the
165/// same shape family, so node-level claims alone do not explain what executed.
166/// The reason travels with the variant so a trace explains both.
167#[derive(Clone, Debug, PartialEq, Eq)]
168pub struct KernelVariantSelection {
169 /// Stable implementation name suitable for filtering and aggregation.
170 pub variant: &'static str,
171 /// Human-readable explanation of the dispatch predicate that selected it.
172 pub reason: Cow<'static, str>,
173}
174
175impl KernelVariantSelection {
176 /// Construct a selected variant with its dispatch reason.
177 pub fn new(variant: &'static str, reason: impl Into<Cow<'static, str>>) -> Self {
178 Self {
179 variant,
180 reason: reason.into(),
181 }
182 }
183}
184
185/// Trace-argument key carrying the selected kernel implementation.
186pub const ARG_KERNEL_VARIANT: &str = "kernel_variant";
187/// Trace-argument key carrying why the kernel variant was selected.
188pub const ARG_KERNEL_VARIANT_REASON: &str = "kernel_variant_reason";
189
190/// Whether kernel-variant trace annotations would currently be recorded.
191///
192/// This is true only when an enabled executor op-span is active on the current
193/// thread, so callers can guard shape-dependent reason formatting behind it and
194/// keep the disabled dispatch path allocation-free. It is the cheap thread-local
195/// check that closes the "dead write" gap: without a live span, an annotation
196/// has nothing to attach to, so there is no point formatting a reason.
197#[must_use]
198#[inline]
199pub fn kernel_variant_tracing_enabled() -> bool {
200 onnx_runtime_tracer::tracing_active()
201}
202
203/// Record a selected kernel variant on the active runtime trace span.
204///
205/// The annotation enriches the per-op span the executor opens for a traced run,
206/// so it carries the node identity already. Callers should normally use
207/// [`record_kernel_variant!`] so formatted reasons are built only when a span is
208/// active.
209#[inline]
210pub fn record_kernel_variant_selection(selection: &KernelVariantSelection) {
211 if !kernel_variant_tracing_enabled() {
212 return;
213 }
214 onnx_runtime_tracer::annotate_current_span_with(|| {
215 onnx_runtime_tracer::Args::new()
216 .with(ARG_KERNEL_VARIANT, selection.variant)
217 .with(ARG_KERNEL_VARIANT_REASON, selection.reason.as_ref())
218 });
219}
220
221/// Record a selected kernel variant for a named *sub-decision* of the current
222/// op.
223///
224/// A single claimed node can make several independent kernel-path choices in
225/// sequence (e.g. `GroupQueryAttention` picks a prep-fusion path *and* an
226/// attention-backend path). Recording each under the shared
227/// [`ARG_KERNEL_VARIANT`] key would let a later choice overwrite an earlier one
228/// on the same span, so each sub-decision is namespaced by `stage` into
229/// `kernel_variant.<stage>` / `kernel_variant_reason.<stage>`. Use the plain
230/// [`record_kernel_variant_selection`] for the node's terminal/primary variant.
231#[inline]
232pub fn record_kernel_variant_stage_selection(stage: &str, selection: &KernelVariantSelection) {
233 if !kernel_variant_tracing_enabled() {
234 return;
235 }
236 let variant_key = format!("{ARG_KERNEL_VARIANT}.{stage}");
237 let reason_key = format!("{ARG_KERNEL_VARIANT_REASON}.{stage}");
238 onnx_runtime_tracer::annotate_current_span_with(|| {
239 onnx_runtime_tracer::Args::new()
240 .with(variant_key, selection.variant)
241 .with(reason_key, selection.reason.as_ref())
242 });
243}
244
245/// Record a selected kernel implementation and lazily formatted reason on the
246/// active executor op-span.
247///
248/// Formatting is skipped entirely unless a span is active
249/// ([`kernel_variant_tracing_enabled`]), so instrumented dispatch sites stay
250/// allocation-free on the hot decode path when tracing is off.
251#[macro_export]
252macro_rules! record_kernel_variant {
253 ($variant:expr, $($arg:tt)+) => {{
254 if $crate::kernel_variant_tracing_enabled() {
255 let selection = $crate::KernelVariantSelection::new(
256 $variant,
257 ::std::format!($($arg)+),
258 );
259 $crate::record_kernel_variant_selection(&selection);
260 }
261 }};
262}
263
264/// Record a named *sub-decision* kernel variant on the active executor op-span.
265///
266/// Like [`record_kernel_variant!`] but namespaces the annotation under `$stage`
267/// so multiple kernel-path choices made while executing one node do not clobber
268/// each other. Formatting is skipped entirely unless a span is active.
269#[macro_export]
270macro_rules! record_kernel_variant_stage {
271 ($stage:expr, $variant:expr, $($arg:tt)+) => {{
272 if $crate::kernel_variant_tracing_enabled() {
273 let selection = $crate::KernelVariantSelection::new(
274 $variant,
275 ::std::format!($($arg)+),
276 );
277 $crate::record_kernel_variant_stage_selection($stage, &selection);
278 }
279 }};
280}
281
282/// Decline the current capture-support query with an actionable reason.
283///
284/// Formatting is evaluated only on the decline path.
285#[macro_export]
286macro_rules! decline_capture {
287 ($($arg:tt)+) => {
288 return $crate::CaptureSupport::unsupported(format!($($arg)+))
289 };
290}
291
292/// Require a capture precondition, declining with an actionable reason when false.
293///
294/// Formatting is evaluated only when the condition fails.
295#[macro_export]
296macro_rules! require_capture {
297 ($condition:expr, $($arg:tt)+) => {
298 if !$condition {
299 $crate::decline_capture!($($arg)+);
300 }
301 };
302}
303
304/// Decline the current `supports_op` call with an actionable reason.
305///
306/// Formatting is evaluated only on the decline path.
307#[macro_export]
308macro_rules! deny {
309 ($($arg:tt)+) => {
310 return $crate::KernelMatch::unsupported(format!($($arg)+))
311 };
312}
313
314/// Require a claim condition, declining with an actionable reason when false.
315///
316/// Formatting is evaluated only when the condition fails, keeping the hot
317/// supported path allocation-free.
318#[macro_export]
319macro_rules! require {
320 ($condition:expr, $($arg:tt)+) => {
321 if !$condition {
322 $crate::deny!($($arg)+);
323 }
324 };
325}
326
327/// A zero-copy **view output**: a kernel's declaration that one of its outputs
328/// is a strided view aliasing one of its inputs' buffers, rather than freshly
329/// computed bytes (`docs/ORT2.md` §5.4, lazy PyTorch-style views).
330///
331/// The `shape` / `strides` / `byte_offset` describe the output tensor relative
332/// to the **same base pointer** as the referenced input view (i.e. relative to
333/// the input's backing allocation, honoring any offset that input itself
334/// already carried). Strides are in **elements** and may be negative (DLPack).
335/// The executor records this metadata against the output value and does **not**
336/// allocate a buffer or invoke the compute path for that slot; the source
337/// buffer is kept alive until the view's consumers have run.
338#[derive(Clone, Debug, PartialEq, Eq)]
339pub struct ViewOutput {
340 /// Positional index (into the kernel's `inputs`) of the aliased input.
341 pub input_index: usize,
342 /// Output shape.
343 pub shape: Vec<usize>,
344 /// Output element strides relative to the aliased input's base pointer.
345 pub strides: Vec<i64>,
346 /// Byte offset of the output element origin from the aliased input's base.
347 pub byte_offset: usize,
348}
349
350/// An executor-delivered kernel input. Existing EPs receive `Tensor` variants;
351/// an EP advertising the `nxrt` capability may receive a lazy `Weight` at the
352/// `pkg.nxrt::BlockQuantizedMoE` boundary.
353pub enum KernelInput<'a> {
354 Tensor(TensorView<'a>),
355 Weight(&'a WeightHandle),
356}
357
358impl<'a> KernelInput<'a> {
359 pub fn tensor(&self) -> Option<&TensorView<'a>> {
360 match self {
361 Self::Tensor(view) => Some(view),
362 Self::Weight(_) => None,
363 }
364 }
365
366 pub fn weight(&self) -> Option<&WeightHandle> {
367 match self {
368 Self::Tensor(_) => None,
369 Self::Weight(weight) => Some(weight),
370 }
371 }
372}
373
374/// A kernel ready to execute a specific op with specific shapes (§4.2).
375pub trait Kernel: Send {
376 /// Tell the kernel which positional inputs are immutable graph constants.
377 ///
378 /// The session calls this exactly once, immediately after construction.
379 /// Kernels may use it to prepack or memoize those inputs. Runtime inputs must
380 /// never be marked constant: caching them would return stale results.
381 fn set_constant_inputs(&mut self, constant_inputs: &[bool]) {
382 let _ = constant_inputs;
383 }
384
385 /// Execute over device-resident inputs/outputs.
386 fn execute(&self, inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()>;
387
388 /// Execute through the general weight-delivery seam.
389 ///
390 /// The default adapter accepts only resident tensor inputs and forwards to
391 /// [`Kernel::execute`], so existing EPs compile and behave identically.
392 /// Paging-aware kernels override this method to consume lazy handles.
393 fn execute_with_inputs(
394 &self,
395 inputs: &[KernelInput<'_>],
396 outputs: &mut [TensorMut],
397 ) -> Result<()> {
398 let views = inputs
399 .iter()
400 .map(|input| {
401 input.tensor().copied().ok_or_else(|| {
402 crate::EpError::KernelFailed(
403 "kernel received a lazy WeightHandle without implementing \
404 execute_with_inputs"
405 .into(),
406 )
407 })
408 })
409 .collect::<Result<Vec<_>>>()?;
410 self.execute(&views, outputs)
411 }
412
413 /// Estimated FLOPs, if known (for the cost model).
414 fn estimated_flops(&self) -> Option<u64> {
415 None
416 }
417
418 /// Attempt to express this node's outputs as zero-copy [`ViewOutput`]s over
419 /// its inputs instead of computing bytes (the layout/movement-op fast path).
420 ///
421 /// `inputs` carries the real (possibly already-strided) input views and
422 /// `num_outputs` is the node's output arity. Returning:
423 /// * `None` — the default — means "compute normally": the executor allocates
424 /// output buffers and calls [`Kernel::execute`].
425 /// * `Some(specs)` means every output is a view; `specs.len()` MUST equal
426 /// `num_outputs`. A kernel that can view some but not all outputs must
427 /// return `None` (all-or-nothing) so correctness never regresses.
428 ///
429 /// When `Some` is returned, [`Kernel::execute`] is **not** invoked.
430 fn view_outputs(&self, inputs: &[TensorView], num_outputs: usize) -> Option<Vec<ViewOutput>> {
431 let _ = (inputs, num_outputs);
432 None
433 }
434
435 /// Whether the kernel accepts a non-contiguous (strided) input at `idx`.
436 fn supports_strided_input(&self, input_idx: usize) -> bool {
437 let _ = input_idx;
438 false
439 }
440
441 /// The layout the kernel writes most efficiently, if it has a preference.
442 fn preferred_output_layout(&self) -> Option<TensorLayout> {
443 None
444 }
445
446 /// Whether this kernel can participate in device-graph capture.
447 ///
448 /// The provided default is supported because capture is a runtime/EP
449 /// capability: kernels that need restrictions override this method and
450 /// return the exact failed precondition.
451 fn capture_support(&self) -> CaptureSupport {
452 CaptureSupport::Supported
453 }
454
455 /// Compatibility shim for existing CUDA-graph callers.
456 ///
457 /// New capture gates must use [`Kernel::capture_support`] so a decline
458 /// reason is never discarded.
459 fn cuda_graph_compatible(&self) -> bool {
460 self.capture_support().is_supported()
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use std::sync::Arc;
467 use std::sync::atomic::{AtomicBool, Ordering};
468
469 use super::*;
470 use crate::{DeviceId, DevicePtr};
471
472 #[test]
473 fn cost_zero_and_total() {
474 assert_eq!(Cost::ZERO.total_us(), 0.0);
475 let c = Cost::new(10.0, 5.0, 2.0);
476 assert_eq!(c.total_us(), 17.0);
477 assert_eq!(c.bytes_moved, 0);
478 assert_eq!(c.launch_us, 0.0);
479 }
480
481 #[test]
482 fn cost_builders_are_additive() {
483 let c = Cost::new(10.0, 5.0, 2.0)
484 .with_launch_us(3.0)
485 .with_bytes_moved(4096);
486 // launch time folds into the total; bytes_moved is metadata for roofline.
487 assert_eq!(c.total_us(), 20.0);
488 assert_eq!(c.bytes_moved, 4096);
489 }
490
491 #[test]
492 fn kernel_match_reports_support() {
493 let supported = KernelMatch::Supported {
494 cost: Cost::new(1.0, 0.0, 0.0),
495 required_input_layouts: None,
496 output_layouts: vec![],
497 };
498 assert!(supported.is_supported());
499 assert_eq!(supported.reason(), None);
500
501 let unsupported = KernelMatch::unsupported("test EP supports no ops");
502 assert!(!unsupported.is_supported());
503 assert_eq!(unsupported.reason(), Some("test EP supports no ops"));
504 }
505
506 fn require_positive(value: i32) -> KernelMatch {
507 require!(value > 0, "value must be positive, got {value}");
508 KernelMatch::Supported {
509 cost: Cost::ZERO,
510 required_input_layouts: None,
511 output_layouts: vec![],
512 }
513 }
514
515 #[test]
516 fn require_macro_carries_formatted_decline_reason() {
517 let rejected = require_positive(-2);
518 assert_eq!(rejected.reason(), Some("value must be positive, got -2"));
519 assert!(require_positive(2).is_supported());
520 }
521
522 #[test]
523 fn kernel_variant_selection_carries_winner_and_reason() {
524 let selection =
525 KernelVariantSelection::new("gemv", "M==1 decode uses the packed int4 GEMV path");
526 assert_eq!(selection.variant, "gemv");
527 assert_eq!(
528 selection.reason,
529 "M==1 decode uses the packed int4 GEMV path"
530 );
531 }
532
533 #[test]
534 fn record_kernel_variant_without_active_span_skips_formatting() {
535 // No executor span is open here, so `tracing_active()` is false: the
536 // macro must not evaluate its format arguments (the dead-write guard).
537 let formatted = AtomicBool::new(false);
538 record_kernel_variant!("gemv", "{}", {
539 formatted.store(true, Ordering::Relaxed);
540 "M==1 decode"
541 });
542 assert!(!formatted.load(Ordering::Relaxed));
543 }
544
545 #[test]
546 fn record_kernel_variant_annotates_active_span() {
547 use onnx_runtime_tracer::TraceContext;
548 let (trace, events) = TraceContext::in_memory();
549 {
550 let _span = trace.span("MatMulNBits", "op");
551 record_kernel_variant!(
552 "gemv_f16",
553 "K={}, N={} chose the vectorized GEMV",
554 4864,
555 896
556 );
557 }
558 let events = events.events();
559 assert_eq!(events.len(), 1);
560 let args = events[0].args.as_ref().unwrap();
561 assert_eq!(args[ARG_KERNEL_VARIANT], "gemv_f16");
562 assert!(
563 args[ARG_KERNEL_VARIANT_REASON]
564 .as_str()
565 .unwrap()
566 .contains("K=4864, N=896")
567 );
568 }
569
570 #[test]
571 fn record_kernel_variant_stage_namespaces_multiple_subdecisions() {
572 use onnx_runtime_tracer::TraceContext;
573 let (trace, events) = TraceContext::in_memory();
574 {
575 let _span = trace.span("GroupQueryAttention", "op");
576 // Two independent sub-decisions on the same node: the staged prep
577 // choice must survive the terminal attention-backend choice rather
578 // than being overwritten by the shared key.
579 record_kernel_variant_stage!("prep", "gqa_prep_fused", "Sq==1, even head_dim");
580 record_kernel_variant!("attention_gqa_decode_fp16_splitk", "split-K decode");
581 }
582 let events = events.events();
583 assert_eq!(events.len(), 1);
584 let args = events[0].args.as_ref().unwrap();
585 assert_eq!(args[ARG_KERNEL_VARIANT], "attention_gqa_decode_fp16_splitk");
586 assert_eq!(args["kernel_variant.prep"], "gqa_prep_fused");
587 assert!(
588 args["kernel_variant_reason.prep"]
589 .as_str()
590 .unwrap()
591 .contains("even head_dim")
592 );
593 }
594
595 struct DecliningCaptureKernel;
596
597 impl Kernel for DecliningCaptureKernel {
598 fn execute(&self, _inputs: &[TensorView], _outputs: &mut [TensorMut]) -> Result<()> {
599 Ok(())
600 }
601
602 fn capture_support(&self) -> CaptureSupport {
603 CaptureSupport::unsupported("per-call workspace allocation is not capturable")
604 }
605 }
606
607 #[test]
608 fn cuda_graph_compatible_shim_matches_capture_support() {
609 let supported = LegacyKernel {
610 called: Arc::new(AtomicBool::new(false)),
611 };
612 assert_eq!(
613 supported.cuda_graph_compatible(),
614 supported.capture_support().is_supported()
615 );
616
617 let unsupported = DecliningCaptureKernel;
618 assert_eq!(
619 unsupported.cuda_graph_compatible(),
620 unsupported.capture_support().is_supported()
621 );
622 assert_eq!(
623 unsupported.capture_support().reason(),
624 Some("per-call workspace allocation is not capturable")
625 );
626 }
627
628 struct LegacyKernel {
629 called: Arc<AtomicBool>,
630 }
631
632 impl Kernel for LegacyKernel {
633 fn execute(&self, inputs: &[TensorView], _outputs: &mut [TensorMut]) -> Result<()> {
634 assert_eq!(inputs.len(), 1);
635 assert_eq!(inputs[0].shape, &[4]);
636 self.called.store(true, Ordering::Relaxed);
637 Ok(())
638 }
639 }
640
641 #[test]
642 fn legacy_kernel_adapter_receives_the_resident_tensor_path() {
643 let called = Arc::new(AtomicBool::new(false));
644 let kernel = LegacyKernel {
645 called: Arc::clone(&called),
646 };
647 let bytes = [1u8, 2, 3, 4];
648 let shape = [4usize];
649 let strides = [1i64];
650 let inputs = [KernelInput::Tensor(TensorView::new(
651 DevicePtr(bytes.as_ptr().cast()),
652 onnx_runtime_ir::DataType::Uint8,
653 &shape,
654 &strides,
655 DeviceId::cpu(),
656 ))];
657
658 kernel.execute_with_inputs(&inputs, &mut []).unwrap();
659 assert!(called.load(Ordering::Relaxed));
660 }
661}