Repository navigation
Parallelize Kn int4 dequant in prefill load path + profiling (#959) - #970
Merged
Merged
Conversation
Instruments the O(weight-bytes) one-time work MatMulNBits does on the first execution of each node, so the native CPU load/init fixed cost (#959) can be broken down by phase. Gated by the existing ONNX_GENAI_PROFILE_MM=1 switch; zero-cost and byte-identical when unset. Emits per-phase running totals to stderr: - dequant-nk / dequant-kn: int4->f32 expansion in the non-MLAS hand path - mlas-shards / mlas-packed: the MLAS SQNBit repack Phase 1 finding (#959): on the measured build (--features native-backend, no mlas), int4 MatMulNBits weights are dequantized to f32 twice per generate -- once transposed (Kn) for prefill, once contiguous (Nk) for decode -- and run through a dense f32 GEMM. This dequant is the dominant weight-scaling term of the fixed cost. No functional change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #970 +/- ##
==========================================
- Coverage 79.00% 78.99% -0.02%
==========================================
Files 356 356
Lines 150158 150211 +53
Branches 150158 150211 +53
==========================================
+ Hits 118639 118661 +22
- Misses 26868 26897 +29
- Partials 4651 4653 +2
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
🔴 Benchmark Regression DetectedComparison of criterion micro-benchmarks: PR head vs merge-base, measured on the same runner in the same job (base first → PR second).
Visual flags: Host infoWhat this cannot catch
|
The transposed Kn ([k,n]) f32 dequantization consumed by the dense prefill GEMM was a single-threaded scatter loop, while the Nk (decode) layout already had a rayon fast path. On the CPU-only (mlas-off) build this serial phase dominated the cache-cold first prefill: ~5s on qwen0.5b-q4 (~43 s/GB), the bulk of the native-vs-ORT load-time gap in #959. Each Kn depth-row [depth*n .. (depth+1)*n) is contiguous, so we parallelize across depth-rows with par_chunks_mut(n). Byte-identical to the serial scatter (same dequantize_nbits_value, same indices); the grouped path stays serial. Measured qwen0.5b-q4, warm cache, best-of-3, --max-new-tokens 1 (load only): before: load 7.6s wall / 9.6 CPU-s / 1.27 cores; dequant-kn ~5.06s after: load ~4.8s wall / ~3.9 cores; dequant-kn ~2.1s approaching ORT's 3.5s. Refs #959. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
pushed a commit
that referenced
this pull request
Aug 15, 2026
Adds the day's milestones: fabricated device/host capacity removed (#947/#952), symmetric int4's missing zero-copy CPU kernel and its 8x resident footprint (#979, 14B 66 GB -> 7.80 GB), the memory plan accounting for the file rather than the residency and the cache becoming a governed decision (#971/#987), advisory_only made visible (#955), native CPU load found serial rather than expensive (#959/#970, 6.4 s -> 3.5 s), ragged batch geometry (#750 stage 3a), the plugin EP claiming 0 of 585 nodes because kernels were advertised in the wrong domain (#956), two ruled-out hypotheses for the interspersed-partition hang (#982), and placement plus the cost model that would decide it (#994/#995) with the first measured rate (host DRAM 49.28 GB/s at 20 threads). Also records the method note, because it was the day's actual lesson: several of my own intermediate conclusions were wrong and retracted -- a validation that was clean because the knob never engaged, a load/decode split contaminated by a cold cache, a workaround inferred from directory names that was exactly backwards, and a footprint lever that looked free on a short prompt and diverged on a long one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Native CPU-only (mlas-off) load path for int4 no-zero-point models spent its first-prefill time in a single-threaded int4→f32 dequantization (the
Kn/[k,n]transpose consumed by the dense prefill GEMM), while the siblingNk(decode) path was already rayon-parallelized. On this box that serial phase was ~⅔ of load wall and pinned the whole load to ~1.27 cores busy — the bulk of the native-vs-ORT load-time gap in #959.This PR:
ONNX_GENAI_PROFILE_MM=1): env-gated per-phase timing of the one-time weight dequant/prepack (dequant-kn,dequant-nk,mlas-*). Zero-cost / byte-identical when unset.dequant-kn: eachKndepth-row[depth*n .. (depth+1)*n)is contiguous, so it parallelizes withpar_chunks_mut(n)over depth-rows. Byte-identical to the serial scatter (samedequantize_nbits_value, same indices; grouped path stays serial). Nounsafe.Measured (qwen0.5b-q4, CPU-only build, warm cache, best-of-3,
--max-new-tokens 1= load only)Approaching ORT's 3.5 s / ~4.5 cores — the load gap was threading, not a costlier algorithm. Greedy output unchanged (
"The capital of France is Paris.").Scope / what this does NOT fix
Load only. The 14B
~102 s/tokendecode cliff is a separate symptom of the same f32-materialization root cause: the non-mlas decode caches each weight as f32 (weight_nk), an 8× expansion (6.995 GB int4 → ~56 GB f32 > this 63.8 GB box → paging). That needs a footprint fix (blockwise int4 kernels /mlas), not threading — noted on #959, not in this PR.Gates
cargo fmt -p onnx-genai-engine -p onnx-runtime-ep-cpu— okcargo clippy -p onnx-runtime-ep-cpu -- -D warnings— okcargo test -p onnx-runtime-ep-cpu— 1065 passed; 0 failedcargo test -p onnx-genai-engine --lib— 382 passed; 0 failedRefs #959.