Skip to content

fix(dflash): let the drafter see its trained context window (deep-context accept) - #740

Open
dusterbloom wants to merge 8 commits into
Luce-Org:mainfrom
dusterbloom:fix/draft-ctx-floor
Open

dusterbloom wants to merge 8 commits into
Luce-Org:mainfrom
dusterbloom:fix/draft-ctx-floor

Conversation

@dusterbloom

@dusterbloom dusterbloom commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

TLDR — The drafter's feature window was capped at draft_ctx_max (default 4096) and its source ring was only 4096 slots, so spec-decode accept collapsed once the committed prefix exceeded ~4K. This sets the window to the drafter's trained 40960, with one shared helper feeding the target feature ring, the single-GPU mirror and the layer-split mirror, and restores DFLASH_FEAT_RING_CAP as the memory valve. The F32 feature default is kept (lossless; the IPC draft ring is F32-only) and q4_0 is an opt-in. Two latent overflow/aliasing bugs found in adversarial review of the same path are fixed too.

What

Port of eafc5de51 ("fix feature-mirror cap to drafter trained window, "), which never reached main. On current main:

  • qwen35_backend.cpp:661 capped the mirror at min(draft_ctx_max=4096, max_ctx, target_feat_cap);
  • the draft window was sub-floored at max(2048, draft_ctx_max) in the qwen35/qwen35moe spec loops and the shared helper.

Beyond ~4K committed the drafter saw only a shallow suffix, so accept decayed to ~0%.

Why

avg_commit is the whole point of spec decode, and it is gated by how much context the drafter can read. A 4K window on a 16-40K agentic prefix makes the drafter blind to the tokens that matter, and the adaptive controller correctly floors it to plain decode. The window should be the drafter's trained sliding window (named constant DFLASH_DRAFTER_TRAINED_CTX = 40960), not an unrelated 4096 default.

Changes

  • common/dflash_feature_ring.h: DFLASH_DRAFTER_TRAINED_CTX = 40960 + dflash_drafter_window(max_ctx) = min(max_ctx, trained), lowered by DFLASH_FEAT_RING_CAP if set. All three feature producers use it (target ring, mirror, layer-split mirror) so their windows cannot desync.
  • qwen35_backend.cpp, qwen35moe_backend.cpp, common/dflash_spec_decode.cpp: draft_ctx = min(committed, ring_cap) — no 2048/draft_ctx_max sub-floor.
  • qwen35_target_graph.cpp: target feature ring cap follows the same helper (was 4096).
  • common/dflash_feature_ring.cpp: copy_host_capture_slice_to_draft_ring now converts to the ring storage type instead of writing F32-width rows (the opt-in q4_0 ring would otherwise overflow the row on the layer-split host-capture path); DFLASH_FEATURE_DTYPE stays f32 by default (q4_0 is opt-in).
  • copy_feature_ring_range_to_tensor: quantised mirror runs dequantise in one bulk call instead of one launch per row.

Before / After

RTX 3090 (24 GB), Qwen3.6-35B-A3B UD-Q3_K_XL + qwen3.6-35b-a3b-dflash-new-f16-fixed, temp 0, greedy, --max-ctx 32768 --kvflash 8192:

committed prefix before (main) after (this PR)
1.5K ~8% 7.9%
9.1K ~0% (window floored at 4K) 14.2%
15.2K ~0% (window floored at 4K) 13.2%

Peak VRAM 21.6 GiB (f32 default), no OOM; [dflash-feature] mirror dtype=f32 cap=32768. Honest note: the after column is measured on the 3090; the before column is the documented 4K-floor failure mode (verified by inspection + the historical commit's own 8K accept 0%->44.8% measurement), not a fresh A/B — no baseline binary was built.

Tests

test_dflash_window.cpp covers dflash_drafter_window: trained cap, max_ctx clamp, and the DFLASH_FEAT_RING_CAP valve (unset/0/negative/high all fall back to the trained window). Runs in test_server_unit.

Caveats

  • --draft-ctx-max remains the remote-draft / layer-split draft-context knob; the local feature window is now governed by the trained constant + DFLASH_FEAT_RING_CAP.
  • On this synthetic prompt avg_commit ≈ 1.0 (little net speedup); the fix restores depth-stable accept, it does not by itself make the drafter profitable on every workload.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 10 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread server/src/common/dflash_spec_decode.cpp Outdated
Comment thread server/src/common/dflash_feature_ring.cpp Outdated
Comment thread server/src/qwen35/qwen35_target_graph.cpp Outdated
Comment thread server/docs/ENVIRONMENT.md Outdated
Comment thread server/src/common/dflash_feature_ring.cpp Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread server/src/common/dflash_feature_ring.cpp Outdated

@Graffioh Graffioh left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good job! check the cubic review and maybe do another round of astra review (if you dont have the usage i can run it...it is at least good as a reviewer even if sometimes it's silly....)

did you try on the lucebox as well?

Port of eafc5de (never reached main). The feature-mirror cap was
min(draft_ctx_max=4096, max_ctx, target_feat_cap) and the draft window was
sub-floored at max(2048, draft_ctx_max), so beyond ~4K committed the drafter
saw only a shallow suffix and spec-decode accept collapsed (~0% at 8K+).

- mirror cap = min(max_ctx, DFLASH_DRAFTER_TRAINED_CTX=40960), the drafter's
  trained SWA window (constant in dflash_feature_ring.h)
- drop the 2048/draft_ctx_max sub-floor in the qwen35 + qwen35moe spec loops
  and the shared helper: draft_ctx = min(committed, ring_cap)
- default DFLASH_FEATURE_DTYPE to q4_0 (accept-neutral) so the deep ring is
  affordable; f32/f16/bf16/q8_0 still selectable

Verification (accept at 8K/16K on RTX 3090) pending a build.
Adversarial review of the port found two blockers:

- The mirror was raised to 40960 but the target feature ring (the mirror's
  source) stays 4096 slots, so restore/backfill for positions older than 4096
  maps onto other tokens' features (aliased, silent). Raise
  TARGET_FEAT_CAP_DEFAULT to the same trained window so source and mirror span
  the same range.
- copy_host_capture_slice_to_draft_ring wrote hidden*sizeof(float) bytes per
  row with F32 offsets regardless of storage_type; with the q4_0 default that
  is ~4x past the row and OOB for the last slot (layer-split host capture).
  Convert each row to the ring storage type and offset by ggml_row_size.
M2: one dflash_drafter_window() now derives the effective window (trained
40960, clamped by max_ctx) for all three feature producers — the target ring,
the single-GPU mirror, and the layer-split mirror — so they cannot desync, and
DFLASH_FEAT_RING_CAP is restored as the memory valve (lower clamps both
rings). The layer-split path no longer uses the shallow draft_ctx_max window.

M1: copy_feature_ring_range_to_tensor dequantises the whole contiguous run in
one ggml_get_to_fp32_cuda call instead of one launch per row (the q4_0 mirror
can span 40960 rows).
Adds test_dflash_window.cpp (trained cap, max_ctx clamp, DFLASH_FEAT_RING_CAP
valve) and documents DFLASH_FEAT_RING_CAP / DFLASH_FEATURE_DTYPE in
ENVIRONMENT.md.
…mate

Address cubic review on the PR:
- dflash_spec_decode: draft_ctx_max is a REMOTE-only cap (non-positive = no
  cap); the local draft reads the full ring, so the shared 4096 default no
  longer starves the deep window on layer-split.
- default DFLASH_FEATURE_DTYPE back to f32 (lossless; the IPC draft ring is
  F32-only) and document q4_0 as opt-in.
- concurrent_fixed_cache_bytes uses dflash_drafter_window(max_ctx) so the
  auto-memory estimate matches the enlarged target ring.
- ENVIRONMENT inventory entry for DFLASH_FEAT_RING_CAP.
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.

2 participants