Repository navigation
lfm2: draft with LiquidAI's DSpark drafter (--drafter) - #1391
BlivionIaG wants to merge 8 commits into
Conversation
|
A small request, @BlivionIaG: this description links more than 20 issues and PRs, which is over Agent Traffic Control's limit, so ATC drops the PR from its board and collision checks (and marks every check incomplete for everyone). Could you trim the related-work list to the issues this PR actually fixes or depends on (20 at most)? Thanks. |
ashhart
left a comment
There was a problem hiding this comment.
Thanks @BlivionIaG, a lot of careful work in this stack. We mapped all 31 commits to their PRs and reviewed each on current main. For the DSpark drafter (this PR and its descendants):
- Loading changes the released BF16 weights.
dspark_weights.zigquantizes each dense matrix to 4/8-bit codes at startup (8-bit by default), and #1393 still discards the original. Target verification protects final tokens, but the engine doesn't create lossy copies of a checkpoint at serving time. Please run the released BF16 matrices, or refuse the format. Also honour each advertised--drafter-bitsmode or refuse it by name (the native adapter accepts the setting and then uses 8 regardless). - 4-bit mode reads past its buffer. With 4 bits the loader packs
markov_w2at that width, butlfm2_markov_scan(lfm2_dspark.metal) reads a byte per coefficient and the launcher passes no bit width. At 128000x256 the code buffer is 16,384,000 bytes and the shader can address byte 32,767,999 (found statically, not run on a GPU). Please refuse that mode before load or implement it correctly. - The held (asynchronous) path drops draft failure flags: the synchronous path refuses a NaN pick or invalid ID (
DraftNonfinite), butreadPicksafter an unwaited hold ignoresPick.nonfiniteand maps an invalid ID to 0, as does the GPU-held copy. Native serving enables that path, so please carry the flag through the fence and add planted NaN/Inf/invalid-ID controls for both paths. - Contract proof: the new operators report 0 planted controls and no recorded compiler/launch limits (the limit declarations use current-device checks with
offline = false). Please add the gates, red controls, served shapes and records. - Config admission doesn't check
hidden_actbefore the fixed SiLU path, and an explicit scheduled verify without a confidence head silently becomes depth mode; both should refuse by name. - The FP32 reference hard-codes a 6-bit target embedding and its host
matmuljoins workers only after all spawns succeed. - The device receipts are mostly on older bases; we'll need alternating A/B on current main per landing head (the stack's own notes report a 1.8-2.7% four-stream drop on Qwen3.8-27B + DFlash2 from the shared-lane changes, which needs explaining, not dismissing).
|
Thanks @ashhart. Pushed
|
|
Thanks @BlivionIaG. A lot of this is fixed, and fixed well:
What still needs to change:
Once these are in and the stack is rebased on current main without #1379 and #1382, we will run drafted against plain, |
… holds follow the grants A drafter that gives each held chain draft's chance (Drafter.VTable.chances) may set Facts.chances. Drafted.facts then sets Model.draft_probabilities and Model.granted_depth, so the window allocator prices each stream's verify rows by those chances, as it does a head's node chances. With Facts.confidence_floor it instead verifies the drafts at least that likely (a fixed window when 0). With granted_depth a stream's next hold is as deep as its last grant plus a slack: one more draft when the grant took every held draft, none when it stopped short of them (Stream.grant_short), and after a grant of none a probe of draft_slack drafts at the depth rule's probe interval. Facts.slack sets one slack for all three. A shared round's granted depths are cut so the drafting streams' blocks fit one hold pass of batch_rows. When the shared-round guard starts a drafted stretch, its first round grants every stream one draft, so the stretch measures drafted rounds rather than plain ones left by grants of none. All of it is behind Facts.chances and Model.granted_depth, off by default; windows.allocate records grant_short only for granted depths. Tests: chances price the chain's rows and drafted replies equal one-token decode alone and at 1, 4 and 8 shared streams, with the scheduled verify ragged across streams and verifying fewer rows than a fixed window; the facts reach the lane config; granted depths, their slacks and the one-pass cut; holds as wide as the grants.
…py, and holds kept only while they pay The depth rule asks one head draft for a stream with a copy ahead, and a shared round's granted holds were priced only by their verify rows. A drafter whose hold runs as a pass of its own (Facts.copy_first) pays that pass for the one draft, and the copy round then drops it; Model.copy_first now asks it for none. A drafter may also give its hold pass's timed cost by blocks and depth (Facts.hold_costs, priced by hold_price.zig). With granted depths a shared round then keeps granted holds, most expected drafts a row first, only while each raises the next round's expected tokens a ms over its verify rows and the pass; a probe after a grant of none keeps its hold. A stream's last grant records the drafts it expected to keep (Stream.grant_gain). Both are off by default (copy_first false, hold_costs empty). Tests: with copy_first no hold is asked for a copy round and replies equal one-token decode at 1 and 3 streams; a hold's cost between and past its timed passes; granted holds kept while they pay, none for a dear pass, and a probe's hold kept.
Bound the reused ordering buffer by batch_rows, and cover reuse with an allocator that refuses further allocations. Interpolate between the measured depths surrounding the requested depth, including nearest-end extrapolation. Query a stateful copy proposer once for a granted-depth budget decision.
The lane core can put an external drafter in front of a target. The drafter reads the target's hidden rows after some layers (its taps). LFM2-MoE had no taps, so it could draft only with the core's own heads. prepare_features names the layers. From then on, each forward copies each row's output of those layers into the scratch (lfm2_tap, a plain copy of bf16 values). features() gives a stream's rows of the latest verify or one-token round. prefill_features hands each prompt chunk's rows to the drafter before the chunk's end is kept for reuse. The prompt's last row is kept beside the stream's caches for its first draft. A drafting stream resumes only a kept state that also holds its drafter's context (Snap.draft). Without a drafter nothing changes: no taps are kept and every forward runs as before. lfm2_tap registers with the kernel contract (metal_lfm2_tap.zig): the third of five taps' columns at 1, 10, 32 and 2,048 rows must hold the layer's bits and the other columns what the call found. Gate 5 checks its launch against every Apple chip we serve and this GPU. test-lfm2-dspark checks the same copy through the family's own kernels.
35e9255 to
8e9b548
Compare
|
@ashhart Rebuilt on current main |
Adds the LFM2.5 DSpark drafter using the released BF16 matrices by default, explicit 8-bit opt-in, and 4-bit refusal. Target verification keeps accepted output exact.
Rebuilt on main
d33dcfc767ee093393c6bfa00b3949bd7b4d8e88. The rejected #1379/#1382 guard commits are excluded. No M5 was accessed for this restack. Fresh CI is running; maintainer re-review remains required.Scope and review fixes
Includes clean #1390 taps and #1381 pricing. Removes the shared guard; fixes buffer-before-list failure cleanup with scratch/warm allocation-failure coverage; refuses rank >256 and nonfinite theta; widens quantizer arithmetic in Debug; refuses --drafter when local wider-lane qualification leaves width one. Keeps failure flags and kernel contracts.
Validation
Head
8e9b548fd. 67/67 scoped build steps, 353 tests passed and three skipped. The drafter reference and kernel-contract executables compile. Lean has zero new findings. BF16/8-bit device results from earlier receipts are historical until the fresh local replay is recorded.Remaining performance requirement
The existing M5 integration loses about 1–2% at four/eight streams while gaining about 7–9% at 16. This restack does not claim to fix or waive that tradeoff, and Ash has not approved an exception to the no-slowdown requirement. The accepted task-owner tradeoff is not presented as maintainer approval. No stream-count/context guard is introduced.
Part of #1374. The chain’s intermediate branches are now updated rather than leaving fixes only in the cumulative tip.
The new cumulative M4 replay is now recorded on #1394: full source/hardware scope and results. It qualifies the final integration, not a separate run of each intermediate head.