Skip to content

feat(vindex3): a reduced-depth draft is a physical plan, not a shard (ANE-4A0) - #311

Open
chrishayuk wants to merge 1 commit into
mainfrom
worktree-ane4a-qwen-drafter
Open

feat(vindex3): a reduced-depth draft is a physical plan, not a shard (ANE-4A0)#311
chrishayuk wants to merge 1 commit into
mainfrom
worktree-ane4a-qwen-drafter

Conversation

@chrishayuk

Copy link
Copy Markdown
Owner

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:

arm result
hybrid LLLF fixture, Draft{L} vs Full identical logits, final_hidden, executed_layers
hybrid fixture, Draft{L-1} executes 0..L-1, finite, differs
Draft{0} / Draft{L+1} refused, with the right reason
Granite 4.1 3B, real container, 40 layers 100,352 logits BIT-IDENTICAL
CLI, --draft-depth 40 vs no flag, production backend BYTE-IDENTICAL

The fixture arms run 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

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, so ExecutionSlice is 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.

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_layers and PreparedOperands::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.
  • The recurrent-state precheck is scoped to the executed range. 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.

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.

…(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.
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