Repository navigation
perf(ArcInfer/ArcSample): STEP_us on V4's real arm; ARC_SAMPLE_ON_DEVICE ends the 66 MB/step host copy that walled off the GPU sampler; 3 fast-path aggravators - #222
Merged
Conversation
…it onto the non-paged step arm V4 actually runs The only TOTAL/fwd/sample/other instrument lived on the PagedAttention arm, which V4 can never reach (DeepSeekV4Loader::supports_paged_attention is false). The DefaultInstructions arm — the path V4 runs — had never once reported how a step divides between GPU and host. Log format byte-identical to the paged arm's so tooling parses both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK8VgocBUrr5djjE1ZfCNf
…evice-eligibility predicate
(a) has_penalties(): a DRY config at its disabled default (multiplier 0.0,
a proven no-op in apply_dry_penalty) no longer disqualifies the GPU
fast path or is_raw_argmax.
(b) sample_top_kp_min_p: the server defaults (top_k=-1, top_p=1.0,
min_p=0.0) ran a full-vocab sort_unstable_by (129,280 elements on V4,
per sequence per token) whose result nothing consumed — skip it.
Seeded draw provably identical (thread-local sort counter + pinned
token 506 in tests).
(c) sample_multinomial: the global rng MutexGuard was held (temporary
lifetime extension) across get_top_logprobs and tokenizer.decode —
narrowed to the draw.
Plus Sampler::device_sampling_terminates, the single-source mirror of
sample()'s device dispatch that ARC_SAMPLE_ON_DEVICE partitioning consults,
and the ignored $0 host-cost probe at V4 vocab (D14: host evidence only).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WK8VgocBUrr5djjE1ZfCNf
…eeded token parity for ARC_SAMPLE_ON_DEVICE Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK8VgocBUrr5djjE1ZfCNf
…ippy-by-hand for mistralrs-core) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK8VgocBUrr5djjE1ZfCNf
Code Metrics Report━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Language Files Lines Code Comments Blanks ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ C Header 5 305 210 52 43 CSS 2 1181 1036 34 111 CUDA 81 29475 20161 6275 3039 Dockerfile 1 39 22 8 9 JavaScript 16 3546 2676 482 388 Jinja2 7 694 656 5 33 JSON 75 4896 4893 0 3 Makefile 1 6 5 0 1 Metal Shading Lan| 33 12224 9431 1142 1651 PowerShell 1 300 227 30 43 Python 147 15371 12677 824 1870 Shell 42 10304 6858 2769 677 Plain Text 4 3801 0 2479 1322 TOML 33 1498 1294 54 150 YAML 3 25 23 2 0 ───────────────────────────────────────────────────────────────────────────────── HTML 4 2687 2604 43 40 |- CSS 2 543 479 37 27 |- JavaScript 1 1233 1215 12 6 (Total) 4463 4298 92 73 ───────────────────────────────────────────────────────────────────────────────── Jupyter Notebooks 4 122 83 23 16 |- Markdown 1 60 30 22 8 |- Python 1 122 113 1 8 (Total) 304 226 46 32 ───────────────────────────────────────────────────────────────────────────────── Markdown 213 48932 0 38081 10851 |- BASH 72 1655 1203 331 121 |- C 4 19 19 0 0 |- CUDA 2 84 56 16 12 |- JSON 19 779 779 0 0 |- PowerShell 1 1 1 0 0 |- Python 23 1008 787 113 108 |- Rust 68 2063 1727 78 258 |- TOML 6 207 164 0 43 |- YAML 5 41 36 5 0 (Total) 54789 4772 38624 11393 ───────────────────────────────────────────────────────────────────────────────── Rust 681 340988 293252 18096 29640 |- Markdown 504 32562 471 28063 4028 (Total) 373550 293723 46159 33668 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Total 1353 516771 363188 99077 54506 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
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.
Parent system: ArcInfer / ArcSample. Four changes, ordered by the brief's priority; the instrument merges on its own merits even if the flag work is debated.
1. STEP_us instrument on the arm V4 actually runs (port, ~20 lines)
The only
STEP_us TOTAL/fwd/sample/othersplit lived on the PagedAttention step arm (pipeline/mod.rs:1461-1482post-change), which V4 can never reach (DeepSeekV4Loader::supports_paged_attentionis false — the arm's own comment says so). TheDefaultInstructionsarm — the path V4 runs — had never once reported how a step divides between GPU and host. Ported verbatim: same skip-4/every-25 cadence, byte-identical log format, so tooling that parses one parses both.fwdwrapsforward_inputs;samplewraps the sample-and-dispatch region; cachepre_op/post_op, the logits D2H and the split land inother— which is exactly the bucket that ranks the host candidates below.2.
ARC_SAMPLE_ON_DEVICE— device-resident sampling for B > 1 (default OFF)Premise, verified on this checkout: at any
n_seqs > 1,host_copy_batched_result(pipeline/mod.rs, theCausalGeneration => n_seqs > 1arm) copies the ENTIRE[B, 1, vocab]logits tensor to the host — 256 × 129,280 × 2 B = 66.2 MB/step at B=256, serial, on the critical path.Sampler::sample's fast path then disqualifies every row because the logits are now on CPU (!logits.device().is_cpu(), thetrivialgate), sending all 256 rows into the CPU pipeline the code itself prices at ~5.9 ms/token. The function's own doc states the trap for B=1 ("moving a B=1 batch to the host would silently disable it") and never follows it to B>1. The GPU sampler is structurally unreachable at every batch above 1. No premise in the brief was refuted; one nuance: the O(B²) split-then-copy regression is already fixed on this base — the remaining defect is the O(B) full copy itself plus the disqualification.Change (flag OFF = bit-for-bit shipped behaviour):
Sampler::device_sampling_terminates(sampler.rs) — the single-source mirror ofsample's dispatch: trivial gate minus the device term, then greedy / no-sort / shared-memory-sort / CUDA-radix geometry. Lives next to the gates it mirrors so it cannot drift silently.split_batched_logits_for_sampling(pipeline/mod.rs) — eligible rows stay device views (zero bytes moved); CPU-bound rows are gathered viaindex_selectinto fresh row-sized storage and shipped in ONE batched D2H. The gather order is load-bearing:to_device/to_vec1copy the whole backing storage of a view (CudaStorage::to_cpu_storage, candle9586979,tensor.rs:1949/2379), so gather-then-copy is what keeps the B² term dead. All-eligible → 0 copies; none-eligible → exactly the flag-off single copy.DefaultInstructionsarm only. The speculative/MTP pipelines overridestepentirely and never see it; the PagedAttention arm is untouched (V4 cannot reach it).capability_reachability.rs(Status::Live); flag read by value viaenv_flag_is_set, latched in aOnceLock.Correctness bar: seeded sampling with the flag on produces IDENTICAL tokens to flag-off on CPU-checkable fixtures — asserted for every mask shape (all-device, all-host, both interleavings) against the literal flag-off path (
host_copy_batched_result+index_bs): tokens[23, 42, 31, 22], pinned so both-sides drift cannot pass as parity.Interaction with #217:
is_greedy_trivial(new on this base) readshas_penalties()and therefore inherits fix 3a below — a disabled-default DRY no longer blocks the ArcGraph device-loop either. Same argument applies:apply_dry_penaltyis a proven no-op atmultiplier == 0.0.3. Three measured aggravators
(a)
has_penalties()counted a disabled DRY.dry_params.is_some()answered true for the disabled default (multiplier: 0.0,DrySamplingParams::default()), thoughapply_dry_penaltyreturns without touching a logit at exactly 0.0 — disqualifying the fast path andis_raw_argmaxfor requests that merely carried the struct. Nowis_some_and(|p| p.multiplier != 0.0).(b) Server defaults ran a 129,280-element sort nothing read.
top_k = -1(theadd_request.rsdefault),top_p = 1.0,min_p = 0.0:sample_top_kp_min_pstill ran the full-vocabsort_unstable_byviapartial_sort_top_k(probs, n, true)— whose three consumers (zero-rest, top-p walk, min-p scan) are all inert under those params. Skipped straight to the multinomial;probsuntouched either way, so same rng stream ⇒ same token.(c) The global rng MutexGuard outlived the draw. Temporary lifetime extension through
&mut *rng.lock()kept the guard alive acrossget_top_logprobs(a partial sort plus up totop_ntokenizer decodes) andtokenizer.decode— serialising every concurrently-sampling sequence behind work that never touches the rng. Narrowed to the draw.$0 CPU probe — HOST-cost evidence ONLY, not GPU validation (D14)
Ignored test
host_cost_probe::time_cpu_sample_at_v4_vocab(release, Apple Silicon host, vocab 129,280, CPU tensor, T=0.7; 3 warmup + 30 timed iters). The parity fixture'sVOCABstays 4096 — bumping it breaks that fixture's nucleus-containment premise — so the probe carries its own 129,280 constant.top_k=32top_k=-1, top_p=1.0The server-default row is fix 3b: the full-vocab sort (plus its 1 MB pair-Vec) was ~80-96% of per-row CPU sampling cost at V4's vocabulary. This prices the host pipeline only; it says nothing about GPU behaviour.
Tests + mutation checks (assertion values both ways)
disabled_dry_is_not_a_penalty_and_enabled_dry_is: disabled DRY ⇒is_raw_argmax() == trueand sampled token equals no-DRY token; enabled (multiplier 0.8) ⇒false. Mutated (revert todry_params.is_some()): fails at "multiplier = 0.0 … must not disqualify" (is_raw_argmax= false).server_default_top_k_skips_the_unconsumed_sort: skip path sorts 0 times (thread-localPARTIAL_SORT_CALLSdelta), pre-fix path spelled out literally sorts 1, both draw token 506 on seed 42 (pinned). Mutated (skip removed): failsleft: 1, right: 0("server defaults must skip the sort entirely").each_active_filter_keeps_the_sort_alive: top_p=0.9 ⇒ 1 sort, min_p=0.1 ⇒ 1, top_k=32 ⇒ 1.[23,42,31,22]) are unmoved, i.e. the draw's position in the rng stream did not shift.device_sampling_split_tests: mixed batch = 1 host copy and the gathered storage holds exactlyn_cpu × vocabelements (21 = 3×7) while eligible rows stay views of the full batch (42); all-eligible = 0 copies; none-eligible = 1 copy (flag-off parity). Ramp fixture makes any host-position mapping error a visibly wrong slice. Counter-delta tests serialise on a test-only lock (the counter is process-global and was racy under the parallel test runner).capability_reachability(12) anddoc_citations(11) suites green on the rebased head.Known adjacent facts (report only — outside my file fence)
seq.get_toks().to_vec()runs twice on the engine thread (sampling.rs,sample_sequence+ its llguidance re-sample arm), and the response phase is a serial for-await. Owned by the device-loop/completion lane; not touched here.Ok(None): pure-top-p nucleus not contained in the candidates) falls back to the CPU path and pays a per-row D2H — and if the batch logits are already F32,to_vec1on the shared-storage view prices that at a full-batch copy for that row. Statically predictable declines are already excluded bydevice_sampling_terminates; the runtime case is counted bygpu_sampling_health::DECLINED. If the box A/B shows DECLINED > 0 under real traffic, the one-line hardening is a row-narrowing copy insampling.rs's prologue — which is outside this PR's fence, so it is named here instead of shipped.What this PR does NOT do
pipeline/sampling.rs,engine/mod.rs, or the PagedAttention arm.🤖 Generated with Claude Code
https://claude.ai/code/session_01WK8VgocBUrr5djjE1ZfCNf