Skip to content

Make speculative decoding honor structured-output grammars - #4

Draft
ropoctl wants to merge 14 commits into
QuixiAI:mainfrom
ropoctl:dsv4-custom-tools-stack
Draft

Make speculative decoding honor structured-output grammars#4
ropoctl wants to merge 14 commits into
QuixiAI:mainfrom
ropoctl:dsv4-custom-tools-stack

Conversation

@ropoctl

@ropoctl ropoctl commented Aug 18, 2026

Copy link
Copy Markdown

This is a dependency-stacked draft for local DSV4 Metal validation. It currently includes the open work from #2 (M1 Ultra campaign), #3 (Metal async-output completion), and #1 (Responses custom tools); review the DSpark commits after those dependencies land.

The new work:

  • applies scheduler-provided grammar mask rows to each sequential DSpark draft sample
  • keeps verified and speculative grammar matchers separate and recovers rollback drift
  • supports both full target vocabularies and reduced Qwen-style draft vocabularies
  • repairs wrapped-tokenizer special-token metadata so BOS/control tokens cannot enter permissive regex payloads
  • hardens Metal draft-token completion and invalid-row rejection sampling
  • updates the apply_patch example to use genai-pyo3 with the openai_resp provider

Local DSV4 evidence: the original reasoning-boundary grammar crash was eliminated and grammar-aware DSpark reached mean acceptance 6.00 with observed 80-100% draft acceptance. The run then exposed the tokenizer metadata defect: BOS token 0 was legal inside filename /(.+)/. The offline real-tokenizer regression now rejects BOS while preserving ordinary filename tokens. The server was stopped at the user request before a post-fix end-to-end rerun, so this remains a draft rather than claiming full apply_patch acceptance.

Validation:

  • tests/v1: 21 passed, 4 skipped (MPS-only cases unavailable in the test runner)
  • custom-tool/parser/profile suite: 183 passed
  • Ruff and git diff --check passed

No model-envelope literals or DeepSeek token IDs are added to the inference core; DSML, Qwen, Kimi, and Muse syntax remains in their structural-tag/chat-format adapters.

auroter and others added 14 commits August 14, 2026 21:50
Prefill and decode optimization for dsv4-xxs-1 on Apple Metal, plus the
cleanup pass that made the tree PR-ready.

Prefill: 2048-token chunk 79 -> 371.6 tok/s (4.7x; 134% of the ds4
reference's 277). Tiled MoE prefill GEMM (qc_moe_mm_map0 + 64-slot
dual-half iq2_xxs tile, q2_K SoA down twin), dense-causal MMA prefill
FA over pre-decoded scratches, fused compressor front (cr=4/cr=128),
wide-tile transposed-store q8_0 qgemm, prefill-width mhc, and a
max_num_batched_tokens fix (2048 -> 2176) that stops the scheduler
splitting a 2048-token prompt into two chunks.

Decode: 15 -> 30.5 tok/s end-to-end on the 1k-in/2k-out anchor.
Per-step marshalling memos, fused mhc pre/post, dsv4 router top-k,
sum-folded q2_K down projection, split-K sparse-MLA, load-time q2_K
SoA repack, fp16-direct kv insert, single-dispatch inverse RoPE.

Gates: 8-tok, off1-2000, and 2500-token anchors bit-exact (shas plus
spec-decode counters) across the cleanup; six kernel oracles pass;
prefill walls at the standing baseline. Method and evidence in
perf/optimization_status.md and perf/baseline_status.md.

Known open issues, documented in-code and in perf/: the async-output
completion-event race at multi-chunk request completion (boot ramp
protocol in perf/prefill_handoff.md; fix planned as a separate PR) and
the step-tape diagnostic pending route re-validation.
…ltra campaign

Semantic resolutions:
- qc_mmvq: campaign mb fast path keeps precedence for its gated envelope
  (batch 2-8, q8_0/q2_K/iq2_xxs/q6_K); Muse-Glimmer's qgemv_mm row-block
  walk becomes the general path, with the q8_0-small summation-order
  carve-out extended to it.
- rms_norm: single binding dispatches bf16 contiguous inputs to the
  Muse-Glimmer fixed-D kernels and everything else (DSV4 fp16 strided
  q/k splits) to the w32/strided variants; duplicate binding and Python
  wrapper removed; Muse encodes labeled for the CB census.
- compressor: NaN-hunt write-site debug hooks composed with the
  comp_full_compress phase bracket.
- tk_launch.h: both launch_qgemv_mb and launch_qgemv_mm retained.
- dsv4-xxs-1 metal profile: main's 262144/16 GiB sizing retained with the
  campaign's fp16 dtype and 2176 batched-token reserve; long-context note
  rewritten (Metal indexer chain now implemented, boot-ramp caveat).
- benchmark harness: --dump-completions and chars_per_token both kept.
- notebooks: both campaign histories merged chronologically.
- metallib rebuilt from merged kernel sources; extension rebuilt; all six
  Metal kernel oracles pass.
… M1 Ultra campaign

Two conflicts, both trivial: build_attn_metadata grew both sides' new
kwargs (our num_computed_tokens_cpu, main's steady_cache), and the
notebook keeps both appended sections in chronological order. The steady
uniform-decode path is inert on Metal (requires FULL CUDA-graph mode and
the VLLM_STEADY_DECODE_META opt-in). Profile tests pass (48/48).
…fill host guards

Verified all 8 findings from the QuixiCore-Metal PR QuixiAI#3 review against this
tree; applied what holds, documented what was already guarded, rejected one.

- mla fp8 insert (all three kernels): build 2^-e from the float bit pattern
  instead of fast-math exp2, which measures 2 ulps low at negative integer
  inputs on M1 Ultra (probe in perf/results/2026-08-17/coderabbit_fixes/).
  Matches the indexer kernels and the exact fp32 reference; cached codes
  change only for blocks with amax > 448. Scale byte derives from the same
  clamped ei (bit-identical in the reachable exponent range). Anchors must
  be re-gated on next boot (see notebook).
- multi-row MoE GEMV hosts: N-divisibility now enforced — a8_vec routes
  non-multiple N to the one-row kernel, swiglu/sum TORCH_CHECK. The mr grid
  ceil-divides N and tail simdgroups read weight rows past N before the
  store guards; every DSV4 dim divides, so serving behavior is unchanged.
- prefill FA: nc/ns % 32 TORCH_CHECK enforcing the _pad_slots contract;
  contract documented on the kernel and both launchers.
- tk_launch.h: host-checked contracts documented on the router (<=1024
  experts, topk <= 8), indexer compress (cr == 4 vs HISTORY_MAX), compress
  front (cr in {4,128}) and prefill launchers.
- moe_mm_id: AoS qs base is 4-byte aligned (84*blk+16), not 16 — comment
  corrected before it justified a bad widening.
- rms_norm: 256-thread dispatch contract documented (shm slots are only
  written by simdgroups that run).
- save_partial_states: bf16 score+ape add documented as required for bit
  parity with the Triton reference (review suggested widening; rejected).

Rebuilt metallib; full kernel suites pass (prefill FA oracle, 36 tiled-GEMM
checks, SoA/sum6 bit-identical, compress-front c128 bitwise, indexer topk).
Fast-math metal::exp2 is 2 ulps low at negative integer inputs, so decode
was reconstructing every typical KV scale slightly below what the insert
kernels used. Reconstruct 2^(e-127) exactly from the float bit pattern via
a shared mla_ue8m0_scale helper. Metallib rebuilt; all six Metal kernel
suites pass. Anchors re-gated with the insert-scale fix in one boot.
Fresh ramped boot on the fixed build: 8-tok 573db39598e7, off1-2000
bb83cc3054a3 (57.3 s, step ms unchanged), 2500x64 f75e1d41ac3d. All
deterministic across repeat runs; wall shifts are acceptance-mix effects
of the re-rolled trajectories. baseline_status UPDATE 30.
The engine could park forever in MPSEvent::synchronize on the async-output
copy event -- GPU idle, signal never delivered -- most often on a boot's
first multi-chunk prefill (deterministic 2/2 in the 08-14 bisect, again on
08-15), forcing a boot-ramp ops protocol.

Root cause: MPS exposes exactly one stream (torch.Stream(mps) always
returns stream_id 0), so AsyncOutput's CUDA cross-stream choreography
(set_stream, copy_stream.wait_stream(main_stream), generic torch.Event
recorded on the fake copy stream) buys no overlap on Metal and routes step
completion through an event that can lose its signal on timing-sensitive
boots.

On Metal, skip the stream context, wait_stream, and generic Event; enqueue
the same non-blocking D2H copies on the only stream and record a native
torch.mps.Event there for get_output() to wait on. VLLM_QC_ASYNC_OUT_DRAIN=1
swaps the event for a full torch.mps.synchronize() drain as an ops
fallback (measured -3.7% decode, so not the default). CUDA/ROCm path is
byte-identical.

Validated over five fresh dsv4-xxs-1 boots: all pinned anchors bit-exact
(8-tok, off1-2000, 2500x64), decode 32.0 tok/s (best wall of the day),
both historical wedge-trigger protocols clean, kill-switch boot clean.
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 49a8075d-ecfd-4cfb-b3c0-68963a678940

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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