Skip to content

perf(cpu-ep): vectorise RoPE and size its Rayon tasks by work, not by layout - #1175

Merged
justinchuby merged 5 commits into
mainfrom
squad/leon-p7-rope
Aug 18, 2026
Merged

justinchuby merged 5 commits into
mainfrom
squad/leon-p7-rope

Conversation

@justinchuby

Copy link
Copy Markdown
Owner

What

Two changes to the CPU EP's RotaryEmbedding kernel:

  1. rotary_units_per_task — Rayon tasks are now sized by work, not by layout. The kernel used to fan out one task per layout unit: one [B,H,S,D] plane or one [B,S,H·D] row. On rope_llama3_s128 that is 4096 elements (16 KiB) per task, and the scheduling cost swamped the work. Tasks now aggregate whole units until each clears 16 Ki elements, targeting ~4 tasks per worker for balance, and collapse to a single serial pass when the pool has one worker.
  2. Explicit AVX2 arms for both rotation conventions (rotate_split_half, rotate_interleaved). The inner loop was left to the autovectoriser, which did not take it.

Plus a test-only change: the A/B transform grid now generates interleaved=1 (GPT-J) RoPE models, which were previously uncovered.

Why

The old fan-out made the parallel path slower than the serial path at every thread count. Measured on rope_llama3_s128, exec_kernel.compute p50, native-only:

threads parallel (before) serial (before)
1 162 µs 126 µs
8 142 µs 126 µs
16 195 µs 127 µs

Fanning 128 tasks out over a one-worker pool alone cost 36 µs of pure Rayon bookkeeping.

After both changes, same measurement: 242 → 85 µs (t=1), 174 → 67 µs (t=8), 138 → 59 µs (t=16).

Why the vector arms avoid FMA

_mm256_fmsub_ps would drop the intermediate rounding of cos·x1, so the 8-wide body and the scalar tail would disagree in the last ulp on the same tensor. The same graph would then produce different bits depending on how a thread count happened to split it, which flips the A/B parity check on scheduling alone. Separate _mm256_mul_ps + _mm256_sub_ps is bit-identical to the scalar loop. This kernel moves 8 bytes per 3 flops — it is bandwidth-bound, so fusing the multiply is not measurable anyway.

Measured: paired interleaved A/B vs ORT 1.27 CPU EP

One driver invocation, scripts/ort_ab/ab.py, 7 trials × 80 runs × 20 warmups per cell, --threads sets both --native-threads and --ort-intra-threads. base = merge-base 8219fa250, new = this branch. Ratio is native/ORT p50, lower is better; bracket is the min–max band across trials.

model t base ratio new ratio new native p50
rope_llama3_s1 1 1.347 [1.334–1.366] 1.278 [1.251–1.291] 0.006 ms
rope_llama3_s1 8 1.349 [1.307–1.404] 1.273 [1.221–1.311] 0.006 ms
rope_llama3_s1 16 1.399 [1.342–1.441] 1.320 [1.275–1.327] 0.008 ms
rope_llama3_b8_s1 1 1.227 [1.185–1.258] 1.032 [1.007–1.054] 0.011 ms
rope_llama3_b8_s1 8 1.310 [1.253–1.718] 1.145 [1.092–1.307] 0.012 ms
rope_llama3_b8_s1 16 1.028 [0.719–1.169] 0.881 [0.797–0.961] ✅ 0.017 ms
rope_llama3_s128 1 1.642 [1.631–1.653] 0.800 [0.791–1.046] ✅ 0.091 ms
rope_llama3_s128 8 4.372 [3.526–6.329] 3.890 [3.149–4.139] 0.127 ms
rope_llama3_s128 16 7.209 [6.217–9.948] 6.111 [4.966–6.830] 0.189 ms
rope_llama3_s512 1 1.258 [1.163–1.286] 0.929 [0.853–0.945] ✅ 0.431 ms
rope_llama3_s512 8 4.485 [4.413–5.668] 3.437 [2.221–4.180] 0.296 ms
rope_llama3_s512 16 7.036 [6.439–8.881] 5.574 [4.888–5.908] 0.348 ms
rope_gptj_il_s1 1 1.400 [1.375–1.429] 1.301 [1.287–1.313] 0.006 ms
rope_gptj_il_s1 8 1.385 [1.296–1.432] 1.299 [1.269–1.358] 0.006 ms
rope_gptj_il_s1 16 1.424 [1.351–1.453] 1.300 [1.267–1.329] 0.008 ms
rope_gptj_il_s128 1 1.911 [1.900–1.920] 0.874 [0.861–0.909] ✅ 0.098 ms
rope_gptj_il_s128 8 3.724 [3.369–5.509] 3.628 [2.779–5.536] 0.124 ms
rope_gptj_il_s128 16 7.486 [7.140–8.042] 6.146 [5.976–6.773] 0.195 ms
rope_gptj_il_s512 1 1.530 [1.494–1.560] 0.964 [0.954–0.974] ✅ 0.434 ms
rope_gptj_il_s512 8 4.533 [4.336–5.130] 3.008 [2.418–3.450] 0.264 ms
rope_gptj_il_s512 16 7.655 [7.503–9.390] 5.808 [5.037–7.063] 0.377 ms

21 / 21 cells improved. 0 regressed. 5 cells now beat ORT outright. Parity 294/294 PASS.

This does not finish RoPE

Five cells win; sixteen still lose. The remaining loss is not in this kernel — it is thread-pool wake latency, and it is the same root cause behind every "wins at t=1, loses at t=8/16" row in the master benchmark doc (§23.7: Softmax, Transpose, Concat, MoE all have that shape).

Isolated measurement, 16 workers, one 524 288-element elementwise region that costs 388 µs on one thread:

gap between regions region cost
none (back-to-back) 67 µs
20 µs 226 µs
100 µs 230 µs
500 µs 236 µs

A 20 µs gap — less than the serial glue between two graph nodes — turns a 5.8× speedup into a 1.7× slowdown. Rayon parks its workers at the end of every region; ORT's intra-op pool spins first (ALLOW_INTRA_OP_SPINNING, default on), so its workers are still hot when the next op arrives and ours are not. exec_kernel.compute for rope_llama3_s128 at t=16 is 59 µs against ORT's ~30 µs, but the paired wall is 189 µs — the difference is wake latency, not arithmetic.

This repo already has the right mechanism: decode_spmd's persistent pool with a bounded KMP_BLOCKTIME-style active-spin window. It is wired only to the decode GEMM path. A prototype routing this kernel's fan-out through it takes rope_llama3_s128 t=16 from 79 µs to 25 µs native-only, past ORT's 30 µs — but it oversubscribes when an ORT session shares the process, so it needs SMT-aware worker placement to land. That is the follow-up, tracked separately; this PR is the kernel half and stands on its own.

Tests

New, each falsified before being kept:

  • avx2_split_half_is_bit_identical_to_the_scalar_loop — sweeps half across 1…128 so the tail runs at every residue mod 8; compares to_bits(), not a tolerance. Falsified: swapping one _mm256_mul_ps/_mm256_sub_ps pair for _mm256_fmsub_ps fails it at half=16.
  • avx2_interleaved_is_bit_identical_to_the_scalar_loop — the de-interleave uses a lane permutation that is its own inverse; a wrong index vector still yields plausible magnitudes, so this compares every element. Falsified: replacing the permutation with the identity fails it at half=8.
  • task_chunking_covers_every_unit_exactly_once — the derived tiling must be a partition for 4 × 4 × 5 shape/pool combinations, because it feeds par_chunks_mut where an off-by-one silently drops or double-writes a row.
  • a_single_worker_pool_never_splits_the_rotation
  • tasks_clear_the_minimum_work_bar_when_the_pool_is_wide — falsified: removing the minimum-task-size floor fails it with "task of 8192 elements is below the 16384-element bar".

Existing suites, all green after merging origin/main (0bd8397f7):

  • cargo test -p onnx-runtime-ep-cpu --lib → 1328 passed, 0 failed
  • cargo test -p onnx-runtime-ep-cpu-plugin --test plugin_ort_e2e → 53 passed, 0 failed (real ORT 1.27 session, session.disable_cpu_ep_fallback=1)
  • cargo fmt --all --check, cargo clippy -p onnx-runtime-ep-cpu --all-targets → clean

No defer

No node is handed to ORT's CPU EP by this change. The claim policy is untouched; no_supported_node_is_ever_left_to_the_ort_cpu_ep and every_fixture_loads_with_cpu_fallback_disabled still pass.

Host

AMD EPYC 9V74, 16 physical cores / 32 threads, one NUMA node, 32 MiB shared L3, AVX2+FMA (no AVX-512). Shared and contended, so only paired ratios from a single driver invocation are quoted; absolutes drift by >4×.

Leon and others added 4 commits August 18, 2026 02:20
… layout

The RotaryEmbedding kernel fanned out one task per layout unit - one
`[B,H,S,D]` plane or one `[B,S,H*D]` row. On `rope_llama3_s128` that is
4096 elements (16 KiB) per task, and the scheduling cost swamped it: the
parallel path was *slower* than the serial one at every thread count
(t=1 162us vs 126us, t=8 142us vs 126us, t=16 195us vs 127us). The
kernel also left its inner loop to the autovectoriser, which did not
take it.

Two changes:

* `rotary_units_per_task` aggregates whole layout units until each task
  clears 16 Ki elements, targeting ~4 tasks per worker for balance, and
  collapses to a single serial pass when the pool has one worker.
* `rotate_split_half` / `rotate_interleaved` gain explicit AVX2 arms.
  They use separate multiply/add rather than FMA so the vector body and
  the scalar tail round identically - a tensor must not change value
  because a different thread count split it in a different place.

Measured `exec_kernel.compute` p50 on `rope_llama3_s128`, native-only:
242 -> 85us (t=1), 174 -> 67us (t=8), 138 -> 59us (t=16).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The transform grid only covered `interleaved=0`. The GPT-J convention
rotates adjacent even/odd channels, which is a structurally different
inner loop, so it needs its own cells in the A/B matrix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ound

Opus adversarial review of #1175 verified the permutation algebra, the
target_feature gating and the chunking arithmetic, and raised four
non-blocking findings. All four applied:

* The SAFETY comments claimed an "AVX2+FMA probe"; the probe checks
  `avx2` only, and deliberately so - the arms use no FMA intrinsic.
* The `half` sweeps only reached residues {0,1,3,7} mod 8, so the
  comment's "every residue" claim was false. Both sweeps now cover
  1..9 plus 13, 14, 20 so every residue is hit before and after several
  full vector iterations. Falsified: deleting either scalar tail now
  fails at half=1 (it did not before).
* No shape drove the parallel re-chunk with `total_units` indivisible
  by `per_task`, so the short final slab was only proved correct by
  hand. Added (1,130,32,128) 3D and (2,66,17,64) 4D. Falsified: an
  off-by-one task stride now fails the sweep.
* `vector_rotation_available` is used only in this module; narrowed
  from `pub(crate)` to private.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby
justinchuby marked this pull request as ready for review August 18, 2026 02:48
@justinchuby
justinchuby merged commit a50258c into main Aug 18, 2026
11 of 17 checks passed
@justinchuby
justinchuby deleted the squad/leon-p7-rope branch August 18, 2026 02:54
justinchuby added a commit that referenced this pull request Aug 18, 2026
)

## Why

Two problems with the master benchmark doc as it stands on `main`:

1. **§21.2/§21.3 are stale.** They describe the input-binding copy as an
open gap and explicitly decline to say what closing it would be worth.
#1146 closed it — `bind_inputs` on `rope_llama3_s128` went from ~601 µs
to 0.7 µs. Every ratio in the document that predates that merge is being
read against the wrong baseline, and nothing in the document says so.
2. **§23.7 frames the remaining losses as five independent kernel
problems.** They are mostly one problem, and it is not in any of those
kernels.

## What this adds

### §24 — Phase 6 (#1146)
What the borrowed `DeviceBuffer` does and the exact preconditions; the
sequence-storage choke point (`read_seq_element` *moves* a buffer into
cross-run `shared_buffers`, so guarding the slot was not enough); the
57-cell result (56/57 improved, 342/342 parity PASS); and precisely
which §21 claims it supersedes.

### §25 — Phase 7 (#1175)
The two independent `RotaryEmbedding` defects, with the measurement that
made them visible once #1146 left `exec_kernel.compute` at 96% of the
run:

* the fan-out was **net-negative at every thread count** (t=16: 195 µs
parallel vs 127 µs serial; 36 µs of pure Rayon bookkeeping to fan 128
tasks over a *one-worker* pool);
* the inner loop was scalar at ~1.4 cycles/element.

Plus why the new AVX2 arms deliberately avoid FMA (a tensor must not
change value because a different thread count split it in a different
place), and the 21-cell matrix: 21/21 improved, 0 regressed, 5 now beat
ORT.

### §26 — the root cause behind the `t=8/16` column
An isolated 524 288-element region on 16 workers, varying only the idle
gap between regions:

| gap | region cost |
|---|---|
| none | 67 µs |
| 20 µs | 226 µs |
| 500 µs | 236 µs |

A 20 µs gap — less than the serial glue between two graph nodes — costs
~160 µs. Rayon parks its workers; ORT's intra-op pool spins first, so
its workers are hot when the next op arrives and ours are not.
`rope_llama3_s128` at t=16 is 59 µs of kernel against ORT's ~30 µs, but
a 189 µs paired wall.

`decode_spmd` already implements the right policy (bounded
`KMP_BLOCKTIME` spin) and is wired only to the decode GEMM path. A
prototype routing RoPE through it takes t=16 from **79 µs to 25 µs
native-only, past ORT's ~30 µs**.

It is documented rather than merged, for two reasons that are both worth
recording:

**(a) A latent correctness hazard.** `SharedState::publish` writes the
job into a single `UnsafeCell` slot (`decode_spmd.rs:243`); `wait`'s doc
comment states "the dispatcher is a single, never-idle thread" as an
assumption, not an invariant. Two concurrent dispatchers overwrite each
other's job pointer and both sets of workers run whichever closure
landed last — **silently wrong tensors, not a crash**. Found
empirically: new fan-out unit tests reported 6 tasks run when 5 were
dispatched, and separately 0, depending on what else in the 1350-test
binary was dispatching; `--test-threads=1` passes. A per-module CAS
guard is *not* sufficient because the 11 existing dispatch sites do not
take it. Latent today (the decode engine dispatches inline on one
thread, and the plugin EP calls `disable_persistent_decode_pool()`),
live as soon as two sessions in one process both run decode GEMM — which
is exactly the concurrency the paged-KV work has to preserve.

**(b) Two spinning pools oversubscribe.** With the prototype on and an
ORT session alternating in-process, t=16 is bimodal — 0.030 ms on one
run, 0.441 ms on the next, same binary, same command.
`ONNX_GENAI_CPU_DECODE_THREADS=16` confines the process to CPUs 0–15,
which on this host is **8 physical cores** (siblings are adjacent). A
blocktime sweep over 500/200/50/20/5/0 µs gave ratios from 0.233 to 72.3
with no monotone trend. The section also records that the compact mask
is *correct* and must not be "fixed" to spread — one thread per core
across 0,2,…,30 straddles two CCXs and measured worse (0.133 vs 0.079
ms).

### §26.3 — the consequence for the work queue
Softmax, Transpose, Concat and the MoE cells should not be attacked as
separate kernel problems until (a) is fixed; their t=8/16 numbers are
mostly measuring thread wake-up, so per-kernel tuning against that
column will have to be redone. The stated exception is §11.5's
paged/appendable KV cache, which is a real algorithmic cost no scheduler
change touches.

## Scope

Documentation only — one file, append-only. No code, no test, no
behaviour change.

Co-authored-by: Leon <leon@squad.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

🔴 Benchmark Regression Detected

Comparison of criterion micro-benchmarks: PR head vs merge-base, measured on the same runner in the same job (base first → PR second).

ℹ️ Absolute times are informational only — they vary with runner load. The % change column is the reliable signal because both sides ran under identical conditions.

Status Scenario Base PR Change
🔴 matmul/medium_generic_f16_threads=8/32x512x512 32.41 µs 44.13 µs +36.2%
🔴 sampling_latency/top_p_per_token 405.11 µs 545.32 µs +34.6%
🔴 sampling_latency/top_k_per_token 52.75 µs 70.07 µs +32.8%
⚠️ sampling_latency/greedy_per_token 3.33 µs 4.26 µs +28.1%
⚠️ qwen3_sampling_processors/top_k_partial_selection 160.36 µs 201.51 µs +25.7%
⚠️ matmul/medium_generic_bf16_threads=8/32x512x512 444.23 µs 557.51 µs +25.5%
⚠️ matmul/large_generic_f32_threads=8/32x1024x1024 3.96 ms 4.87 ms +22.9%
⚠️ sampling_latency/min_p_per_token 223.93 µs 273.91 µs +22.3%
⚠️ qwen3_sampling_processors/top_k_top_p_full_sort_baseline 6.27 ms 7.67 ms +22.2%
⚠️ qwen3_sampling_processors/top_k_full_sort_baseline 2.32 ms 2.83 ms +22.0%
⚠️ logit_processing/seven_processor_chain_per_step 345.32 µs 417.86 µs +21.0%
⚠️ qwen3_sampling_processors/top_p_full_sort_after_top_k_baseline 3.98 ms 4.77 ms +20.1%
⚠️ matmul/large_generic_f32_threads=1/32x1024x1024 9.53 ms 11.33 ms +18.9%
⚠️ kv_cache/alloc_dealloc_pages 43.70 µs 51.07 µs +16.9%
✅ add/large_f32_threads=1-internal/4194304 913.20 µs 1.05 ms +14.7%
✅ grammar_masking/llguidance_compute_mask/32 85.70 µs 97.79 µs +14.1%
✅ matmul/large_generic_f16_threads=8/32x1024x1024 93.70 µs 105.50 µs +12.6%
✅ add/medium_f16_threads=1-internal/262144 118.54 µs 132.74 µs +12.0%
✅ qwen3_sampling_processors/top_p_fast_after_top_k 568.93 µs 627.41 µs +10.3%
✅ qwen3_sampling_processors/top_k_top_p_fast 750.79 µs 822.36 µs +9.5%
✅ matmul/large_generic_f16_threads=1/32x1024x1024 89.23 µs 96.63 µs +8.3%
✅ tokenization/decode_tokens_per_second 6.85 ms 7.26 ms +6.0%
✅ block_quantized_moe_cached_dense/mxfp4_cached_dense_expert_repeated_call/rows=1,H=256,I=256,E=4,top_k=1 176.66 µs 186.57 µs +5.6%
✅ gather/medium_f32_threads=1-internal/32768 4.27 µs 4.44 µs +4.0%
✅ add/large_f16_threads=1-internal/4194304 1.82 ms 1.85 ms +1.6%
✅ matmul/large_generic_bf16_threads=1/32x1024x1024 2.16 ms 2.20 ms +1.6%
✅ add/medium_bf16_threads=1-internal/262144 122.76 µs 124.52 µs +1.4%
✅ add/medium_f32_threads=1-internal/262144 26.86 µs 27.17 µs +1.2%
✅ reduce_mean/small_f32_threads=1-internal/4096 16.88 µs 17.01 µs +0.8%
✅ gather/small_bf16_threads=1-internal/4096 536.6 ns 539.2 ns +0.5%
✅ matmul/large_generic_bf16_threads=8/32x1024x1024 1.59 ms 1.56 ms -1.9%
✅ gather/medium_f16_threads=1-internal/32768 2.87 µs 2.77 µs -3.6%
✅ block_quantized_matmul_cached_dense/mxfp4_preexpanded_dense_oncelock_like_proxy/1x1024x1024 75.01 µs 72.06 µs -3.9%
✅ gather/small_f16_threads=1-internal/4096 544.9 ns 523.0 ns -4.0%
✅ add/large_bf16_threads=1-internal/4194304 1.99 ms 1.87 ms -5.9%
✅ matmul/medium_generic_bf16_threads=1/32x512x512 617.03 µs 571.91 µs -7.3%
✅ block_quantized_moe_cached_dense/mxfp4_uncached_expert_dequant_each_call/rows=1,H=256,I=256,E=4,top_k=1 583.64 µs 532.12 µs -8.8%
✅ tokenization/encode_tokens_per_second 444.31 µs 404.02 µs -9.1%
✅ matmul/medium_generic_f16_threads=1/32x512x512 40.32 µs 36.30 µs -10.0%
✅ add/small_bf16_threads=1-internal/1024 500.3 ns 450.4 ns -10.0%
✅ reduce_mean/large_f32_threads=1-internal/262144 1.12 ms 1.00 ms -10.4%
✅ reduce_mean/medium_f32_threads=1-internal/65536 305.73 µs 270.95 µs -11.4%
✅ gather/medium_bf16_threads=1-internal/32768 2.96 µs 2.60 µs -12.0%
✅ block_quantized_matmul_cached_dense/mxfp4_cached_dense_repeated_call/1x1024x1024 82.12 µs 71.49 µs -12.9%
✅ add/small_f16_threads=1-internal/1024 545.9 ns 474.2 ns -13.1%
🟢 matmul/medium_generic_f32_threads=1/32x512x512 2.80 ms 2.33 ms -16.8%
🟢 matmul/small_generic_bf16_threads=1/1x256x256 40.31 µs 32.72 µs -18.8%
🟢 gather/small_f32_threads=1-internal/4096 917.3 ns 739.1 ns -19.4%
🟢 matmul/small_generic_f16_threads=1/1x256x256 39.98 µs 31.25 µs -21.8%
🟢 matmul/small_generic_bf16_threads=8/1x256x256 41.16 µs 31.50 µs -23.5%
🟢 matmul/small_generic_f16_threads=8/1x256x256 41.28 µs 31.14 µs -24.6%
🟢 gather/large_bf16_threads=1-internal/131072 16.47 µs 12.25 µs -25.6%
🟢 block_quantized_matmul_cached_dense/mxfp4_uncached_dequant_each_call/1x1024x1024 1.04 ms 766.73 µs -26.2%
🟢 matmul/small_generic_f32_threads=1/1x256x256 50.09 µs 36.82 µs -26.5%
🟢 gather/large_f16_threads=1-internal/131072 17.18 µs 12.24 µs -28.7%
🟢 add/small_f32_threads=1-internal/1024 305.7 ns 201.0 ns -34.2%
🟢 matmul/small_generic_f32_threads=8/1x256x256 54.01 µs 33.74 µs -37.5%
🟢 gather/large_f32_threads=1-internal/131072 52.29 µs 31.40 µs -39.9%
🟢 matmul/medium_generic_f32_threads=8/32x512x512 1.68 ms 992.47 µs -40.9%

Visual flags: ⚠️ ≥ 15% slower, 🔴 ≥ 30% slower — calibrated against measured runner noise (~27% worst-case on multi-threaded matmul)

Host info
CPU: Apple M1 (Virtual)
Cores: 3
OS: Darwin 25.5.0 arm64
Rust: rustc 1.97.1 (8bab26f4f 2026-07-14)
Load avg: { 5.41 4.73 6.66 }
What this cannot catch
  • Regressions in code paths not covered by these benchmarks (e.g., end-to-end decode with a real model)
  • Sub-threshold regressions that compound over multiple PRs
  • Performance changes that only manifest under GPU execution
  • Latency changes in the ORT integration path (these benchmarks exercise the native Rust kernels)

@codecov

codecov Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.88136% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.38%. Comparing base (bc1e97f) to head (f8fb1d8).
⚠️ Report is 20 commits behind head on main.

Files with missing lines Patch % Lines
...nnx-runtime-ep-cpu/src/kernels/rotary_embedding.rs 97.88% 2 Missing and 3 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##           main    #1175       +/-   ##
=========================================
+ Coverage      0   80.38%   +80.38%     
=========================================
  Files         0      357      +357     
  Lines         0   154551   +154551     
  Branches      0   154551   +154551     
=========================================
+ Hits          0   124235   +124235     
- Misses        0    25764    +25764     
- Partials      0     4552     +4552     
Flag Coverage Δ
offline 80.38% <97.88%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...nnx-runtime-ep-cpu/src/kernels/rotary_embedding.rs 93.63% <97.88%> (ø)

... and 356 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant