feat(vindex3): a reduced-depth draft is a physical plan, not a shard (ANE-4A0) - #311
Open
chrishayuk wants to merge 1 commit into
Open
feat(vindex3): a reduced-depth draft is a physical plan, not a shard (ANE-4A0)#311chrishayuk wants to merge 1 commit into
chrishayuk wants to merge 1 commit into
Conversation
…(ANE-4A0)
**A shallow model over the same container, and it costs what the plan
costs.** `ExecutionSlice::Draft { end }` executes the embedding, layers
`[0, end)`, then the component's OWN final norm and output head — so its
logits are comparable to the target's, which is what makes it a drafter
rather than a hidden-state shard.
Deliberately not an option on `LayerRange`. A shard is a hidden-state
transform that composes with other shards; a draft is a complete model
that happens to be shallower and owns both ends. Overloading the two
would have made "reduced depth" a flag on something whose semantics are
different.
Prefix-only BY CONSTRUCTION: the variant cannot express `{0, 8, 16, …}`.
In a hybrid stack, omitted recurrent layers own state transitions later
layers consume, so a scattered subset is not yet a defined program. The
type refuses to let that be smuggled in as a parameter.
## The gate: observational equivalence at full depth
A seam that changes the model's logits when it selects every layer is
not an experiment, it is an inference bug — and every reduced-depth
number measured through it would inherit the bug rather than the model.
Bit-identical is the right bar, not a tolerance: both arms run the same
operands through the same backend in the same order.
hybrid LLLF fixture Draft{L} == Full logits, final_hidden,
executed_layers
Draft{L-1} executes 0..L-1, finite,
differs
Draft{0}, Draft{L+1} refused, correct reason
Granite 4.1 3B, real container, 40 layers
Draft{40} == Full 100,352 logits BIT-IDENTICAL
CLI, production backend, real container
--draft-depth 40 vs no flag BYTE-IDENTICAL
The fixture arm runs in CI. The real-container arms are env-gated on
`QW38_CONTAINER` and skip loudly rather than reporting success over a
missing subject.
## The substrate invariant, measured rather than assumed
The cost of executing a reduced physical plan scales with the plan,
not with the size of the authoritative container.
Reading the code suggested this already held. It was measured instead,
because the failure mode reading misses is exactly the interesting one —
execution skipping layers while preparation still reads their weights.
On the real 51 GB Qwen3.8-27B container:
depth layers operand reads reads/layer wall s
1 1 17 17.0 4.47
2 2 31 15.5 5.37
4 4 57 14.2 7.06
8 8 111 13.9 10.47
reads ~ 4 + 13.4 per layer 26.5 GB peak RSS across all arms
Affine in depth. `ExecutionSlice` is therefore already a physical view
over VINDEX3, and no loader change is warranted: `OperandStore::open`
reads only segment headers, `load_raw` seeks to one tensor, and
`PreparedOperands::load` slices the layer range before loading anything.
## The economics this exposed
The stack's ends do not scale with depth and are not small. On Qwen3.8
the embedding table and output head are each 248320 x 5120 — 5.09 GB as
f32 — so a draft pays ~10 GB resident and ~3.6 s whatever its depth. Two
Granite timing points put the same floor at ~11.6 ms/position against
~2.96 ms per layer: 9% of a full-depth run, but a third of a depth-8 one.
A shallow drafter is cheap in layers and expensive in vocabulary. That
is a real constraint on any accepted-tokens-per-millisecond argument,
and no projection microbenchmark showed it.
## Supporting changes
- `ExecutionTrace::executed_layers` — the plan indices that actually
ran. A count cannot distinguish a prefix from a planner falling back
to the whole stack, so the assertions are on identity.
- `PreparedOperands::executed_layers()` — the same identity at
preparation time, for a caller recording provenance.
- The recurrent-state precheck is scoped to the executed range. It
refuses before any output if a recurrence has nowhere to keep durable
buffers; unscoped, a draft was refused — or charged state — for layers
it never runs. `Full` is unchanged.
- `execute_slice(...)`, with `execute_plan` as its `Full` case, so the
two can never disagree about what a whole model means.
## CLI
`larql vindex3 exec --draft-depth N`, one semantic effect, reusing the
existing `--logit-dump` writer so draft planes are directly comparable
to a target bank produced by the same command. A depth deeper than the
model is refused by the slice rather than clamped: a run that silently
served a different depth than it was asked for would poison a ladder.
Each dump now writes a provenance sidecar (backend, draft depth,
executed layers, positions, vocab, prepare/step timing,
`LARQL_CPU_MAX_FORMAT`) so a comparison can ASSERT the arms match rather
than trust a filename. This project has manufactured a "depth effect"
that was a backend effect before.
3495 tests pass, fmt clean, clippy clean.
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.
A reduced-depth model over the same VINDEX3 container, as a physical plan rather than a second model — the substrate ANE-4A needs before it can ask whether a shallow Qwen3.8 predicts the full one well enough to draft for it.
What this adds
ExecutionSlice::Draft { end }— embedding, layers[0, end), then the component's own final norm and output head, so its logits are comparable to the target's.It is deliberately not an option on
LayerRange. A shard is a hidden-state transform that composes with other shards; a draft is a complete model that happens to be shallower and owns both ends. And it is prefix-only by construction: the variant cannot express{0, 8, 16, …}, because in a hybrid stack omitted recurrent layers own state transitions later layers consume, so a scattered subset is not yet a defined program. That is a separate rung, and the type refuses to let it be smuggled in as a parameter.The gate
A seam that changes the model's logits when it selects every layer is not an experiment, it is an inference bug — and every reduced-depth number measured through it would inherit the bug rather than the model. So the bar is bit-identical, not a tolerance:
Draft{L}vsFullfinal_hidden,executed_layersDraft{L-1}0..L-1, finite, differsDraft{0}/Draft{L+1}--draft-depth 40vs no flag, production backendThe fixture arms run in CI. The real-container arms are env-gated on
QW38_CONTAINERand skip loudly rather than reporting success over a missing subject.The substrate invariant, measured
Reading the code suggested this already held. It was measured instead, because the failure mode reading misses is exactly the interesting one — execution skipping layers while preparation still reads their weights. On the real 51 GB Qwen3.8-27B container:
Affine in depth, so
ExecutionSliceis already a physical view over VINDEX3 and no loader change is warranted.OperandStore::openreads only segment headers,load_rawseeks to one tensor, andPreparedOperands::loadslices the layer range before loading anything.What that exposed
The stack's ends do not scale with depth, and on this model they are not small: the embedding table and output head are each 248320 × 5120 — 5.09 GB as f32 — so a draft pays ~10 GB resident and ~3.6 s whatever its depth. Two Granite timing points put the same floor at ~11.6 ms/position against ~2.96 ms per layer: 9% of a full-depth run, but a third of a depth-8 one.
A shallow drafter is cheap in layers and expensive in vocabulary. That is a real constraint on any accepted-tokens-per-millisecond argument, and no projection microbenchmark showed it.
Supporting changes
ExecutionTrace::executed_layersandPreparedOperands::executed_layers()— the plan indices that actually ran. A count cannot distinguish a prefix from a planner falling back to the whole stack, so the assertions are on identity.Fullis unchanged.execute_slice(...), withexecute_planas itsFullcase, so the two can never disagree about what a whole model means.CLI
larql vindex3 exec --draft-depth N, one semantic effect, reusing the existing--logit-dumpwriter so draft planes are directly comparable to a target bank produced by the same command. A depth deeper than the model is refused by the slice rather than clamped.Each dump now writes a provenance sidecar — backend, draft depth, executed layers, positions, vocab, prepare/step timing,
LARQL_CPU_MAX_FORMAT— so a comparison can assert the arms match rather than trust a filename. This project has manufactured a "depth effect" that was a backend effect before.Not in this PR
The depth-vs-agreement ladder itself. That needs a scorer (KL, top-1 agreement, target-token rank, top-k containment) against the banked full-depth target logits, which is the next piece of work.
3495 tests pass, fmt clean, clippy clean.