Skip to content

Optimize performance and add comprehensive benchmark tests#9

Open
Snider wants to merge 178 commits into
mainfrom
dev
Open

Optimize performance and add comprehensive benchmark tests#9
Snider wants to merge 178 commits into
mainfrom
dev

Conversation

@Snider

@Snider Snider commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces several important updates across the documentation and codebase, focusing on improved benchmarking, enhanced documentation for recent engine features and campaigns, and significant performance and feature upgrades in the ngram drafter and welfare guard. The changes also clarify the energy measurement methodology for different engine modes and update command-line options for the welfare guard.

Documentation and Feature Updates:

Major engine and campaign documentation:

  • docs/handover.md: Added comprehensive session and campaign logs, including performance wins, folder retirements, welfare guard deployment, and details on the prompt reuse mechanism and Qwen model integration.
  • docs/release-v0.12.0-DRAFT.md: Clarified energy measurement methodology for "pure replay" (no cache) and stateless prompt reuse, and expanded on the welfare guard's mediation process, including the new lem_end rung and its persistence requirements. [1] [2] [3] [4]
  • docs/examples/energy-ab/README.md: Updated instructions for measuring energy in replay mode to reflect the new default prompt reuse, requiring the kill switch for cache-less measurements.

Engine and Benchmarking Improvements:

Benchmarking and performance:

  • go/decode/generate/generate_bench_test.go: Added a comprehensive set of benchmarks for prompt prefix handling, image data URL decoding, kv storage encoding, and cache mode handling, improving coverage and enabling allocs/B-op tracking.
  • go/decode/ngram/ngram.go: Refactored the ngram drafter's core lookup algorithm to scan only once, anchored on the suffix's last token, reducing work by up to maxNgram× on misses and ensuring byte-identical output with significantly less scanning. [1] [2]

Serve command and welfare guard:

  • go/cmd/lem/serve.go: Added a -welfare flag (default ON) to enable or disable the per-turn welfare guard, and wired the flag into the engine options. [1] [2]

Summary of most important changes:

Documentation and Feature Clarifications

  • Expanded documentation for energy measurement methodology and stateless prompt reuse, including the need for the kill switch to measure pure replay energy. [1] [2] [3]
  • Documented the welfare guard's mediation process, including the new lem_end rung and its persistence, with live deployment details and audit trails. [1] [2] [3]

Performance and Benchmarking

  • Refactored the ngram drafter's lookup to scan only once, greatly improving performance on the miss path. [1] [2]
  • Added detailed benchmarks for prompt handling, image decode, kv storage, and cache mode recognition.

Serve Command Enhancements

  • Introduced the -welfare flag to control the welfare guard at serve time, defaulting to ON. [1] [2]

Snider and others added 30 commits July 11, 2026 14:38
…own push queue, campaign state

Co-Authored-By: Virgil <virgil@lethean.io>
Benchmarks for the pure per-request helpers the chat interceptor pays on
every stateless turn: conversationKey (hashes the retained prefix — twice
per turn), conversationTurnSplit, messagesCarryMedia, normaliseRole.
Realistic shallow (2-turn) and deep (20-turn) transcripts.

Co-Authored-By: Virgil <virgil@lethean.io>
conversationKey assembled the whole retained prefix into an un-presized
core.Builder, paying the builder's doubling-growth reallocation on every
turn (a deep conversation is hashed twice per turn — acquire + sleep).
pprof -alloc_objects put the entire flat allocation at the msg.Content
WriteString. Presize with a computed upper-bound byte budget (len(Role) is
an upper bound for the normalised role; Grow is a capacity hint) so the
prefix assembles in one allocation. Byte-identical key.

Deep (20-turn):    34776 B/op 13 allocs -> 9536 B/op 2 allocs
Shallow (2-turn):    984 B/op  5 allocs ->  576 B/op 2 allocs

Co-Authored-By: Virgil <virgil@lethean.io>
…ant contracts

Benches the per-token and per-load CPU surface of the backend-agnostic model
root: MatNT reference matmul, foldNormBiasOne, NormalizeWrapperNames,
DeriveLayers, the arch/assistant registries, QuantConfig parse+lookup,
ProbeModelTypes, ValidateRequired, Assemble. All pure-Go, no model load.

Co-Authored-By: Virgil <virgil@lethean.io>
mux: the per-token streaming wire encoders — writeResponseDeltaFrame,
writeOllamaChatFrame/GenerateFrame and the shared appendJSONStringHTML
escaper (safe + escaped + long deltas), plus the per-request idWithPrefix.
Locks in the documented zero-per-token-allocation profile (all frame
writers measure 0 allocs/op).
admin: cacheEntryLabelsFrom, the per-request cache-entries label filter.

Co-Authored-By: Virgil <virgil@lethean.io>
Benches the Qwen 3.6 hybrid's per-token CPU surface: SwiGLU MLP, MoE routing
(forward/swigluExpert/topKIndices), full-attention Forward + rmsNormHead +
rotary, the gated-delta mixer adapter, the bf16 boundary conversions, and the
per-load tensorF32 widen + resolveKinds. Pure Go, no model load, no device GEMM.

Co-Authored-By: Virgil <virgil@lethean.io>
…ne (#377)

The stateless text lane opened a fresh session per request: every turn of a
multi-turn chat re-prefilled the whole history (per-turn wall 3.1s -> 4.4s
over ten turns on e2b) and paid a maxLen-sized KV alloc+free round trip —
while llama-server's slot cache stays flat, and two complete LCP reuse
implementations already existed in-engine with no serve-path caller.

TextModel now keeps ONE resident session when the engine declares
PromptReuseCapableModel; stream() prefills through PrefillTokensCached,
which reuses the longest shared prefix of the resident ids (prompt +
generated reply — cachedIDs is maintained by every decode path) and
prefills only the divergent suffix. llama slot-cache parity: single slot,
TryLock (busy -> fresh path), volatile.

Ring safety is the hard rule: sliding layers keep bounded rings, so any
rollback past a wrapped ring (pos > slidingWindow) resumes attention over
rows the discarded tail destroyed — those calls degrade to a cold
PrefillTokens, token-identical either way. Gated both ways in
TestPrefillTokensCached_RingSafety_Ugly.

Stand-down guards: conversation continuity installed (it owns caching;
KV never budgeted twice), LTHN_PROMPT_REUSE=0 kill switch, capability
probe at the model level (no throwaway session opens).

engine suite 181 green, engine/metal suite green; reuse gates:
turn-boundary append reuses prompt+reply rows with token-identical
continuation vs a cold session.

Co-Authored-By: Virgil <virgil@lethean.io>
Hoist the per-token MoE routing scratch out of the token loop: the top-k index
buffer, the expert hidden buffer (sized to the widest expert), and the expert
output buffer are now allocated once per forward call, not once per (token,
expert). swigluExpertInto / topKInto are the buffer-reusing cores; swigluExpert
and topKIndices stay as allocating wrappers for their existing tests. Output is
byte-identical (each buffer is fully overwritten per use) — the 16 composed
tests stay green. FLAT alloc profile confirms the 5 remaining allocs are all
once-per-call setup (out/probs/idx/hbuf/eo), none per-token.

benchtime=20x, D=1024, 8 experts FF=1408, top-2 + shared:
  before  8689277 ns/op  53376 B/op  9 allocs/op
  after   9219715 ns/op  20608 B/op  5 allocs/op

Co-Authored-By: Virgil <virgil@lethean.io>
The existing ThinkingExtractor benches feed single tokens / short blocks,
so they never exercised the cumulative content/thinking totals folded
across a whole answer. These stream a realistic 200-token plain answer and
a channel-reasoning stream token-by-token through one extractor then Flush
— the honest per-response unit, and what exposed the O(n^2) accumulation.

Co-Authored-By: Virgil <virgil@lethean.io>
…600->10/op

ThinkingExtractor accumulated the cumulative Content()/Thinking() totals
with `e.content += text` on EVERY token — quadratic over a stream (each
token recopies the whole growing answer). It also paid a second, per-drain
lazy strings.Builder for the returned delta, duplicating every write.

Fix: make content/thinking core.Builder fields (amortised O(n) growth) and
drop the per-drain delta builder entirely — drain records each builder's
length on entry and returns the tail it grew by (tailFrom), a zero-copy
view that stays valid across later reallocs. Deltas and Content()/Thinking()
are byte-identical; all 702 package tests pass, incl. the plain-token
alloc-budget guard (single-token floor stays 2, now 1 for the delta path).

Process over a 200-token plain answer: 124704 B/op 600 allocs -> 4472 B/op 10 allocs
Reasoning stream:                       10512 B/op 175 allocs -> 1136 B/op 11 allocs
Existing short benches also improved (PlainTokenShort 40->8 B/op, 2->1 alloc).

Co-Authored-By: Virgil <virgil@lethean.io>
Benches the Qwen 3.5/3.6 gated delta-rule linear-attention recurrence at
decode (L=1, carried state) and short prefill (fresh state). Pure Go, no load.

Co-Authored-By: Virgil <virgil@lethean.io>
Benches the RWKV-7 WKV7F32 recurrence (decode + prefill), the full time-mix
BlockForwardF32, and the host-default projMatMul seam. Pure Go, no load.

Co-Authored-By: Virgil <virgil@lethean.io>
Benches the Qwen 3.6 GatedDeltaForwardF32 decode block, Config.Arch +
InferFromWeights per-load derivation, and the registered arch parser via the
model registry. Pure Go, no load.

Co-Authored-By: Virgil <virgil@lethean.io>
Benches the per-token bf16<->f32 seam conversions, the host-default projMatMul,
and the per-weight tensorF32 widen at load. Pure Go, no load.

Co-Authored-By: Virgil <virgil@lethean.io>
runtimememory_bench_test.go and device_bench_test.go bench the
observability/maintenance dispatch — RuntimeMemoryUsage, ClearRuntimeCache,
BackendDeviceInfo — behind the existing fake reporter/provider backends
(no GPU). Hit + miss paths; all measure 0 allocs/op, locking the
registry-lookup + type-assert dispatch cost. The other four root files in
scope (thinking.go, speculative.go, session_contract.go, kvstate.go) are
pure type/interface declarations with no executable work — no benches.

Co-Authored-By: Virgil <virgil@lethean.io>
…-ring rule

prepareAssistantPrompt (the MTP pair serve lane) and GenerateCached /
GenerateCachedSampledEach rolled the session boundary back to the shared
prefix with no ring check. Sliding layers keep bounded rings: landing row p
overwrites the slot of row p-w, so a rollback past a wrapped ring (pos >
slidingWindow) resumes attention over window rows the discarded tail already
destroyed — silent wrong context on multi-turn pair serving whenever the
resent history diverges from the resident ids (thinking-on chats diverge at
every reply, the thought channel is stripped from what clients resend).

All three sites now apply the rule session_prompt_reuse.go established:
a rollback past a wrapped ring degrades to a cold full prefill —
token-identical, just uncached. Gated by TestPrepareAssistantPromptRingSafety
(wrapped-ring divergence decodes token-identical to a fresh session);
assistant suite green.

Co-Authored-By: Virgil <virgil@lethean.io>
…row needs LTHN_PROMPT_REUSE=0

The energy table's third row is now 'pure replay (no cache of any kind)'
with the reproduction note; the draft gains the stateless-is-not-the-
punching-bag paragraph (turn-10 wall 4.4s -> 3.1s, flat, ring-safe,
stands down under continuity). Wake note carries #377 closure + the two
bench-agent worktrees to merge.

Co-Authored-By: Virgil <virgil@lethean.io>
anthropic_stream: the streaming event encoders — AppendContentBlockDelta
(the per-token content_block_delta hot path, safe + escaped), the tool
input_json_delta, and the per-stream message_start/message_delta wrappers.
All the per-token/per-stream builders measure 0 allocs/op (caller-owns-buf,
off the reflect path). tools: RenderToolDeclarations over a realistic
multi-tool Claude Code set. Ollama's chunkenc/unmarshal and anthropic's
jsondec are already covered by the package-level bench files.

Co-Authored-By: Virgil <virgil@lethean.io>
…e benches

Fills the per-file bench gaps left uncovered by safetensors_bench_test.go:
values.go DecodeFloat32/EncodeFloat32 (the whole-file codec, distinct from
index.go's DecodeFloatData), write.go subsetHeaderEncoded + appendJSONInt64
(the pure header emit, distinct from the file-writing WriteSubset), and the
NEON (darwin/arm64) + scalar float16SliceToFloat32 slice conversion. index.go
is deliberately not given a bench file: its symbols are already benched in
safetensors_bench_test.go + header_parse_bench_test.go (no duplication).

Co-Authored-By: Virgil <virgil@lethean.io>
pathWithinDir is the traversal-escape gate on every hot-swap /reload
request. Benches the within / escape / equal branches; the equal fast path
is 0 allocs, the PathRel branches carry the core.PathRel allocation (path
logic, not a hot-loop trap). Closes the admin package's bench gap — the
other admin files are HTTP handlers and boot-time helpers (MachineHash,
JSON I/O), not pure-CPU-benchable in isolation.

Co-Authored-By: Virgil <virgil@lethean.io>
scorer.go (the in-process lem-scorer adapter that rides alongside every
completed turn) was the one pipeline symbol the package bench left
uncovered — pipeline_bench_test.go covers New/Complete/the adapters but not
Score. Benches the typical pair, response-only and empty cases. The typical
pair is ~229us / 99 allocs, dominated by lek.ScorePair + JSONMarshal (both
outside this module's scope) — the adapter's own overhead is a lastUser
read + one string copy + the map; no in-scope trap. types.go is pure
interface declarations; wired.go's adapters are covered by pipeline_bench.

Co-Authored-By: Virgil <virgil@lethean.io>
Benches the GGUF pure-CPU surface: Quantize per format (all nine kernels via
the public entry), the Q8_0/Q4_0 dequantise-to-F16 load path, the quant
classification (NormalizeQuantType/quantBits/quantFamily/buildGGUFTensorInfos/
inferGGUFQuantization), the format dispatch, the metadata coercers, and the
pure header-prep (ggufQuantizeMetadata/assignGGUFTensorOffsets). Synthetic,
no file. quantize_kernels.go is covered via Quantize (no duplicate
direct-kernel benches); metadata.go + tensors_mmap_*.go need a .gguf fixture.

Co-Authored-By: Virgil <virgil@lethean.io>
Two per-request traps killed with the new bench coverage watching:
openai ThinkingExtractor 124704->4472 B/op, 600->10 allocs (quadratic
cumulative concat + doubled per-drain builder, on EVERY streamed token);
continuity conversationKey 34776->9536 B/op, 13->2 allocs (presized).
Bench files land for continuity, compat, openai full-stream thinking,
root dispatch, anthropic stream/tools, admin pathWithinDir, pipeline
scorer; the rest of the census was already benched at package level
(verified per file, no filler duplicates).

Co-Authored-By: Virgil <virgil@lethean.io>
…ffer

Quantize called AppendQuantize(format, nil, values), so the kernel grew the
output from a nil slice block by block (~log2(nblocks) reallocations). The
packed length is deterministic — (values/blockSize)*bytesPerBlock — so pre-size
dst to it and AppendQuantize fills it in one allocation. Byte-identical output
and identical errors (unknown format / non-block-multiple length leave dst nil
and defer to AppendQuantize's check); the 112 gguf tests stay green. FLAT alloc
profile confirms the one remaining alloc is the pre-sized output buffer.

benchtime, 131072 f32 values (512 K-super-blocks / 4096 _0 blocks):
  Q8_0  before 685470 B  23 allocs/op   after 139266 B  1 alloc/op
  Q4_K  before 286702 B  20 allocs/op   after  73922 B  1 alloc/op
  Q2_K  before 212914 B  19 allocs/op   after  49253 B  1 alloc/op

Co-Authored-By: Virgil <virgil@lethean.io>
Quantize/AppendQuantize live in quantize_dispatch.go and resolveGGUFQuantizeFormat/
ggufQuantizeLayout/ValidationSummary in quantize.go — swap the two bench files'
contents so each {file}_bench_test.go benches its own file's symbols. No coverage
change; corrects the per-file mapping only.

Co-Authored-By: Virgil <virgil@lethean.io>
Benches the model-merge pure-CPU surface: linearMerge/slerpMerge/normalizedWeights
(the per-element merge kernels), compareTensorEntries/compareCosine (per-tensor
delta stats), the equalFold/containsFold/hasSuffixFold/clampFloat64 helpers, and
the SamePath/SamePathResolved overwrite guards. Synthetic, no file (Packs/prepare/
indexSources + HashFile need real safetensors, benched via the merge path).

Co-Authored-By: Virgil <virgil@lethean.io>
Benches gemma3's arch derivation (sliding/global layer schedule + dual RoPE
bases) and the shape-inference dim recovery. Synthetic, no file. register.go is
init-only (parser benched via the arch load path); mistral's YaRNInvFreqs is
already benched in its config_bench_test.go (not duplicated).

Co-Authored-By: Virgil <virgil@lethean.io>
Benches the per-record state-store codec: encodeRecordHeader/decodeRecordHeader
(the 24-byte on-disk header, zero-alloc by design) and the hand-rolled JSON path
(extractRecordURI/appendJSONString/appendJSONField/jsonUnescape) the store uses
instead of reflection. Pure Go, no file. The file-backed store ops (put/resolve/
index/store_mmap/helpers) are I/O and exercised via the store path.

Co-Authored-By: Virgil <virgil@lethean.io>
Benches BuildCalibrationPlan (config normalise + sample select + token count) and
boundedCalibrationTokenCount — the native calibration metadata work preceding the
SignRound quantise. QuantizeWeights/PackQuantizedWeights/Dequantize are already
benched in autoround_bench_test.go (not duplicated). Synthetic, no file.

Co-Authored-By: Virgil <virgil@lethean.io>
Benches inferGemma4HeadDim (sliding vs global head dim from q_proj rows) and
inferGemma4PerLayerInputSize (PLE-tower width) — the flagship's don't-guess dim
recovery at load. Config.Arch + Assemble are already benched in the gemma4
bench files (not duplicated). Synthetic tensor set, no file.

Co-Authored-By: Virgil <virgil@lethean.io>
Snider and others added 29 commits July 11, 2026 20:27
… mlx workload replay, fp16 A/B, sustained-clock arm

Five acquittals banked tonight, each with a runnable instrument:
rotating outputs (no WAW serialisation: 0.95x), the mlx wheel's own
metallib through our dispatch (identical 22 TFLOPS — binary acquitted),
float16 vs bfloat16 (1.09x — emulation tax acquitted), sustained 1.3s
burns (22.8 flat — clock ramp acquitted), and their exact spied
workload priced on our dispatches (1.50s vs their 0.74s wall at a
uniform ~22 TFLOPS). The standing contradiction: the same process that
ceilings at 20.5 TFLOPS on a clean 32-op qmm burst (their runtime,
measured tonight) retired 33 TFLOP of spied qmm calls in 0.74s inside
the real forward. Next probe is a Metal capture of their prefill (or
the QQMatmul / compile-fusion hypothesis) — not a guess.

Co-Authored-By: Virgil <virgil@lethean.io>
The missing brick: go-inference has the whole-model quantise pipeline
(gguf.QuantizeModelPack, autoround) and the READER for the MLX native
format (model.QuantConfig + engine/metal affine_qmv), but nothing WROTE
the packed-uint32 + bf16 scales/biases format the engine loads. This
closes the loop — "hand me a bf16, I make the quant variations".

QuantizeTensor implements MLX's affine derivation exactly (reverse-
engineered from and byte-verified against mlx-community's own snapshots):
per inDim-group, the larger-magnitude edge is anchored to an exact code
q0 = round(edge / ((wmax-wmin)/nBins)); scale = edge/q0 (signed negative
when the max-edge dominates); bias = edge (0 when q0==0); codes =
clamp(round((w-bias)/scale), 0, nBins) round-half-away-from-zero. All
float32 — Go's native float32 reproduces the Metal kernel bit-for-bit.
Layout: packed U32 [outDim, inDim*bits/32] LSB-first, scales/biases BF16
[outDim, inDim/groupSize]. Bits 2/4/8 (32%bits==0); 3/5/6 refused (no
reference to verify their cross-word layout — refusing beats wrong bytes).

ConvertSnapshot streams a whole bf16 model dir → a new quantised dir:
eligible tensors (2-D, inDim%groupSize==0) quantised, everything else
(norms, non-aligned matrices) passed through wide, config.json gains the
quantization + quantization_config blocks, tokenizer/template sidecars
copied. One source tensor in memory at a time (header planned from shapes
first), so peak memory is bounded by the largest tensor, not the model.

Eligibility rule verified against the gemma-4-12B 4-bit snapshot: 332
quantised, 292 passed through, ZERO disagreement with mlx_lm.convert.
DequantizeTensor (w = scale*q + bias) added as the round-trip inverse.

Co-Authored-By: Virgil <virgil@lethean.io>
ORACLE VERDICT: BYTE-IDENTICAL. TestByteOracle quantises a sample of
tensors from the cached mlx-community/gemma-4-12B-it-bf16 snapshot and
compares packed/scales/biases byte-for-byte against the 4-bit snapshot
mlx_lm.convert produced from it. All five samples match exactly:

  q_proj  v_proj  gate_proj  down_proj  embed_tokens
  -> 1,658,880 bytes compared, ZERO divergence.

So QuantizeTensor reproduces mlx_lm.convert; the format-authoring loop is
verified against the reader's own producer. No drift to characterise. The
test reads only a slice of leading rows per tensor via the safetensors
ReadAt path (never the 12B model into RAM) and is env-gated on the two
snapshot dirs (LEM_MLX_BF16_DIR / LEM_MLX_4BIT_DIR, or the HF cache),
skipping cleanly when absent — like the repo's other real-model tests.

Unit coverage (no model needed): synthetic round-trip within the group
error bound across bits 2/4/8 and group sizes 32/64 incl. groupSize==inDim;
reconstruction-stability under re-quantise (codes are a fixed point; scale
can move one bf16 ULP — byte-exact idempotence deliberately NOT claimed,
distinct from the oracle's byte-exactness against mlx); constant-group eps
path; shape contract; and the rejection paths (3/5/6-bit, non-dividing
group, length mismatch).

Loadability (TestConvertSnapshot_ToyLoadable): the converter runs on a
tiny synthetic 2-layer model and the output is accepted by the real
config-parse/probe layer — model.ProbeDirArch reads it, the injected
quantization block parses + Validate()s through model.QuantConfig, the
emitted tensors carry the loader's expected U32/BF16 dtypes+shapes, the
sidecar is copied, and dequant reproduces the source within the bound.

Co-Authored-By: Virgil <virgil@lethean.io>
The user-facing verb the quantiser needed. Flag surface:

  lem quant <src-model-dir> [-bits 4] [-group-size 64] [-o <out-dir>] [-gguf <FORMAT>]

Default lane is MLX group-affine (mlxaffine.ConvertSnapshot) at the given
bits/group — the native format the engine loads. -gguf <FORMAT> switches
to the existing GGUF whole-model pipeline (gguf.QuantizeModelPack:
q4_k_m, q8_0, q5_k, q6_k, …). Default output dir mirrors the mlx_lm
convention: <src>-<bits>bit (or <src>-gguf-<format>), with a trailing
-bf16/-f32 tag on the source name stripped first.

Per-tensor progress lines (quantise/copy) to stderr; a final before->after
summary (tensor count, quantised vs passthrough, in/out sizes) to stdout.
A two-phase parse lets the <src-model-dir> positional sit before OR after
the flags (Go's flag stops at the first non-flag), matching the documented
`quant <src> [-flags]` shape.

Registered in main.go under a new "Convert" section (minimal diff).

Smoked through the built binary on a synthetic model:
  - MLX 4-bit: 10 tensors (3 quantised, 1 passthrough), 7184 B -> 2032 B
  - MLX 8-bit: same set, 7184 B -> 3824 B (width flows through)
  - GGUF q8_0: 3 tensors -> model.gguf via the existing pipeline
  - both flag orderings, and the reject paths (no arg / bad bits / no shards)

Co-Authored-By: Virgil <virgil@lethean.io>
…uant)

The competitor-parity verb: lem quant <src> [-bits 4] [-group-size 64]
[-o dir] [-gguf FORMAT]. The new model/quant/mlxaffine package writes
the MLX affine format the engine itself loads — algorithm
reverse-engineered from real input/output pairs and BYTE-IDENTICAL to
mlx_lm.convert (oracle: q/v/gate/down/embed tensors from the cached 12B
bf16 vs mlx-community's own 4bit conversion — 1,658,880 bytes, zero
divergence; the pinned derivation: per-group larger-magnitude edge
anchored to an exact code, signed scale, fp32 arithmetic exactly — fp64
diverges). Eligibility verified against the reference with zero
disagreement (2-D + inDim%group==0; embeddings quantised; norms pass
wide). bits 2/4/8 byte-exact; 3/5/6 refused (no reference to verify
cross-word packing). -gguf lane rides the existing QuantizeModelPack.
Output loadable (probe-gated), config.json quantization block +
mlx-compat alias, sidecars copied. Streams one tensor at a time.

Co-Authored-By: Virgil <virgil@lethean.io>
…lised 45-vs-22 mystery, saved traces

Co-Authored-By: Virgil <virgil@lethean.io>
The deployment-owned filter on model OUTPUT: term/pattern rules with
redact/refuse actions, loaded from a JSON file. The engine ships the
MECHANISM, never a taxonomy — no built-in term lists, only what a
deployment declares. Complements the welfare guard (inbound, the model
adjudicates) with the house rules on output (outbound, the deployer
adjudicates).

A policy compiles ONCE: a bad regexp, an empty term, a refuse rule with
no message, a term carrying a pattern-only window, or an out-of-range
window is rejected at LOAD — never at serve time. Pattern matches are
bounded by a window (per-rule / file / DefaultWindow) so the streaming
enforcer can establish a hold-back bound; an empty-matching pattern is
rejected (its bound cannot be established). Term matching folds ASCII
case; the first-byte dispatch table + hold-back bound are precomputed at
load for the streaming enforcer that follows.

go test ./serving/policy/ — 21 passed (Compile Good/Empty/Bad×13,
HoldBack term/window, Load Good/Bad).
go build ./... clean; go test ./serving/... ./cmd/... — 2064 passed.

Co-Authored-By: Virgil <virgil@lethean.io>
…model_type allow-list

LoadTokenModelDir reached the composed hybrid loader via a hardcoded
model_type switch (qwen3_5/qwen3_6/qwen3_5_moe/…). Replace the composed
arm with model.LoadComposedDir — the neutral ArchSpec.Composed lookup —
so a checkpoint whose model_type registers a Composed hook loads with
zero engine edits (a future qwenX is a model-package init(), not a change
here). mamba2 keeps its by-name branch (it registers no ArchSpec), and a
narrow fallback preserves the historical composed types the registry does
not yet cover (qwen3_6/qwen3_6_moe/composed/hybrid), so every type the old
switch loaded still loads identically.

Pin the routing contract at the model level (runnable without the
metallib): every registered composed model_type resolves through
LoadComposedDir, and a non-composed model_type declines so the caller
falls through to the transformer loader.

Co-Authored-By: Virgil <virgil@lethean.io>
…ction

Groundwork for #380 (O(N^2) per-token re-concat in the streamed reply path).
Introduces the bounded-window detection primitives WITHOUT yet wiring them into
the live handlers, and pins them to the pre-optimisation semantics via a
differential fuzz — so the fix that follows is verified byte-identical before it
lands.

New primitives (unwired this commit; serveStreaming / GenerateStream still run
the old inline loops):
  * openai.contentStreamer — cumulative strings.Builder + a (maxMarkerLen-1)
    rescan window over the tool-call open marker and the request's stop set.
  * serving.streamStopWindow / maxStopLen — the same window over the adapter's
    stop-truncated stream.

Window-bound derivation: a stop sequence or the 12-byte <|tool_call> marker can
only newly COMPLETE within the last (maxMarkerLen-1) bytes of already-emitted
content plus the new token — anything ending inside the already-emitted prefix
would have been detected (and broken / entered-tool on) at an earlier token, so
the loop never needs to rescan there. maxMarkerLen = max(len(ToolCallOpenMarker),
longest stop); a larger-than-needed window stays correct (no marker can sit
wholly inside it — that would have fired earlier).

Oracle = the exact old code path (full `emittedContent + contentDelta` concat,
whole-candidate scans) replicated verbatim. The differential fuzz drives both
paths and requires byte-identical per-token emitted slices, tool/stop boundaries,
final content, inTool state, callback sequence and source-token consumption:
  * handler: 4078 every-boundary splits (15 fixtures) + 20000 random streams
  * adapter: 1452 every-boundary splits (9 fixtures)  + 20000 random streams
Adversarial: markers/stops split at every byte boundary (incl. mid-rune), overlapping
marker prefixes, marker-dense text, unicode + invalid-UTF8 straddling. Native
go-fuzz entry points (FuzzContentStreamer, FuzzStreamStopWindow) seeded for -fuzz.

Benches (oracle vs windowed, 2K/8K-token replies) added to measure the collapse
the wiring commit realises.

Gate: go build ./... ; go test ./serving/provider/openai/ ./serving/ (1101 pass).

Co-Authored-By: Virgil <virgil@lethean.io>
…e hybrid registration

composed now registers top-level model_types (qwen3_5 / qwen3_5_moe /
qwen3_next) through ArchSpec.Composed, but builtin did not import it, so a
serve composition that only imported builtin left the registry entry
inert — the hybrids resolved only in binaries that happened to link
engine/metal's own composed import. Blank-import composed here and correct
the stale "mixer/component packages carry no top-level model_type" comment
(composed does now; deltanet/rwkv7 remain component-only; mamba2 is reached
by the backend's SSM branch, not this registry).

Co-Authored-By: Virgil <virgil@lethean.io>
The hard part done right. Matches span token boundaries, so the enforcer
holds back the minimal disputable tail — HoldBack() bytes = max(longest
term, largest pattern window) — and settles only positions whose full
match window has arrived. A settled decision is byte-identical to running
the policy over the whole text at once; the held tail is re-examined when
the next chunk arrives, or flushed by Close at end of stream.

Byte-exactness: a chunk with no match and no match-prefix in its tail is
emitted as the SAME string (no copy) — terms dispatch by ASCII-folded
first byte, so ordinary text withholds nothing and streams straight
through. redact replaces the matched span; refuse ends the reply with the
rule's message (finish stays clean) and swallows everything after. The
audit Event carries the rule index + action ONLY — never the matched
content, which the deployment may consider sensitive.

Longest-match wins; equal-length ties resolve to the earliest rule.

go test ./serving/policy/ — 37 passed, incl. TestPolicy_Enforcer_
Differential (1000 random chunkings × 4 texts, and the refuse variant)
proving byte-identity across every boundary split, byte-at-a-time
BoundarySpanning, case-fold, precedence, post-refuse swallow.
bench (no-match hot path, b.ReportAllocs):
  BenchmarkPolicy_Enforcer_NoMatch          227.9 ns/op   0 B/op  0 allocs/op
  BenchmarkPolicy_Enforcer_NoMatch_Pattern  2530  ns/op 112 B/op  1 allocs/op
go build ./... clean; go test ./serving/... ./cmd/... — 2080 passed.

Co-Authored-By: Virgil <virgil@lethean.io>
…th (#380)

Wires the contentStreamer / streamStopWindow primitives (landed with their
differential-fuzz oracle in the prior commit) into the live handlers, replacing
the O(N^2) per-token re-scan of the whole accumulated reply.

serveStreaming (openai/handler.go):
  before: candidate := emittedContent + contentDelta   // full concat per token
          core.Index(candidate, ToolCallOpenMarker)     // full scan per token
          firstStopSequenceCut(candidate, stops)        // full scan per token
  after:  out := cs.step(contentDelta)                  // append + bounded window scan

GenerateStream / ChatStream (adapter.go):
  before: full.WriteString(tok.Text)
          truncated := applyStopSequences(full.String(), stops)  // full scan per token
  after:  streamStopWindow(seq, stops, cb)                       // bounded window scan

Detection now scans only the last (maxMarkerLen-1) bytes of already-emitted
content plus the new token — the sole region a stop sequence or the tool marker
can newly complete in. Emitted bytes, tool/stop boundaries, finish reasons,
tool-call extraction, thought deltas and callback sequences are byte-identical
(24078 handler + 21452 adapter differential-fuzz cases, all existing package
tests green unchanged).

Bench (oracle = old path, windowed = new; -benchtime=50x/30x -benchmem):
  contentStreamer 2K:  11324496 -> 46584 B/op (243x), 2048 -> 16 allocs, 1.99ms -> 49us
  contentStreamer 8K: 184349672 -> 211706 B/op (871x), 8197 -> 21 allocs, 27.7ms -> 159us
  streamStopWindow 2K:  20.0ms -> 76us/op (264x)   [bytes flat: the O(N^2) was the scan]
  streamStopWindow 8K: 294.2ms -> 170us/op (1736x)
The N-scaling is quadratic on the old path (2K->8K: contentStreamer B/op 11.3MB->184MB
= 16x for 4x tokens) and linear on the new (46KB->212KB = 4.5x).

Gate: go build ./... ; go vet + go test ./serving/provider/openai/ ./serving/ (1101 pass).

Co-Authored-By: Virgil <virgil@lethean.io>
… falls (#380)

serveStreaming rebuilt candidate = emittedContent + contentDelta and
re-scanned the full reply per streamed token; adapter GenerateStream/
ChatStream full-scanned per token with bytes flat. Detection needs only
the last maxMarkerLen-1 bytes plus the delta (a marker completing
earlier would have fired earlier — derivation in the commit chain);
absolute positions map through a windowStart offset so every emitted
slice is byte-identical. Oracle-first: the verbatim old path retained
as a differential reference; 24,078 + 21,452 fuzz cases (every-byte
boundary splits incl. mid-rune, overlapping prefixes, invalid UTF-8)
require identical emissions, boundaries, callbacks, and consumption.
contentStreamer 8K: 184,349,672 -> 211,706 B/op (871x), 27.7ms -> 159us;
adapter stop-window 8K: 294ms -> 170us (1736x). N-scaling confirms
quadratic -> linear. Native fuzz entry points seeded for CI.

Co-Authored-By: Virgil <virgil@lethean.io>
WrapResolver decorates the resolver so every resolved model's Chat stream
runs through a fresh policy Enforcer. It composes OUTERMOST — after the
welfare wrap — so the policy enforces on the final tokens the deployment
would otherwise emit. A refuse stops consuming the inner stream (the model
stops generating); a clean stream forwards byte-for-byte; end-of-stream
flushes the held tail. One audit line per enforcement (rule index +
action, never the matched content).

`lem serve -policy <file>` loads and compiles the policy up front: absent
means no layer and zero overhead; a load failure is FATAL at boot —
RunServe returns before binding the listener, so a deployer who asked for
a filter never silently serves without it.

Verified with the built binary: `-policy <bad>` exits 1 with
  "serving.RunServe: outbound policy <path> — refusing to serve unguarded:
   policy.Compile: rule #0: compile pattern "[": error parsing regexp ..."
and never binds; `-policy <good>` logs
  "serve: outbound policy ON — 3 rule(s), hold-back 256B; redact/refuse on
   model output, audited per enforcement; -policy disables"
after the welfare line (outermost).

go test ./serving/policy/ — 42 passed (incl. WrapResolver Redact/Refuse/
Passthrough across split tokens, ResolveError propagation, Audit no-leak).
go build ./... clean; go vet clean; go test ./serving/... ./cmd/... — 2085 passed.

Co-Authored-By: Virgil <virgil@lethean.io>
…dels

metalBackend.LoadModel type-asserted *NativeTokenModel and failed every
other loaded model, so a composed hybrid (Qwen 3.6's host-f32 gated-delta
/ full-attention stack) loaded but could not be served — loadComposed
TokenModel's result never reached an inference.TextModel. Wrap it: a
composedTextModel adapts the composed model.SessionModel to engine
.TokenModel (an engine.Session bridge that delegates decode wholesale to
the composed model's own tested token loop, model.GenerateSampledWith
StopTokensTransformEach — no decode logic re-rolled; the recurrent state
has no portable KV snapshot, so the capture methods report that boundary
honestly), and metalBackend.LoadModel returns it through engine.NewText
Model exactly like the native path.

The wrap DECLARES its chat dialect (engine.ChatTemplateDeclarer): ChatML
for the Qwen model_types (config-driven via the new composed.ChatMLDialect
— keyed on config.json's model_type, so a future qwenX is ChatML with zero
edits), the gemma fallback otherwise. A declared template wins over engine
.TextModel's tokenizer detection, so a Qwen checkpoint frames
<|im_start|>/<|im_end|> turns with the <think>\n\n</think>\n\n no-think
block even though its tokenizer carries no <|turn> marker.

Runnable proof (no metallib): ChatMLDialect selection, and ChatML render
+ precedence (declared beats a tokenizer that carries <|turn>) through
engine.NewTextModel, mirroring engine/chat_template_test.go's fake idiom.
The engine/metal wrap itself compiles here but its live serving path needs
the metallib (TestMain gates the package), so it is not runtime-exercised
in this worktree.

Co-Authored-By: Virgil <virgil@lethean.io>
Deployer house-rules on model output — the welfare guard's outbound
sibling with the authority flipped. Mechanism only, no shipped
taxonomy: term rules (ASCII case-insensitive, first-byte dispatch,
0 allocs on the no-match hot path) and pattern rules (compiled at
load, bounded window, unbounded/empty-matching patterns rejected at
load), actions redact/refuse. Streaming enforcement settles only
positions whose full match window has arrived — byte-identical to
whole-text application under a 1000-chunking differential fuzz.
Composed OUTERMOST above the welfare wrap; policy load failure is
fatal before bind; audit lines carry rule index + action, never the
matched content. G2 mediated self-rewrite stays designed-next (#378).

Co-Authored-By: Virgil <virgil@lethean.io>
…ry routing + ChatML declaration (#379)

The wiring agent found the real gap: metalBackend.LoadModel
hard-asserted *NativeTokenModel, so composed models (the whole Qwen
hybrid family) were never servable end-to-end. A composedTextModel
wrap now bridges ComposedTokenModel into engine.NewTextModel via a
session that delegates decode to composed's own tested loop; KV
snapshot/restore reports its boundary honestly (recurrent state has no
portable snapshot yet — multi-turn -state is the follow-up). The
hardcoded version allow-list yields to the ArchSpec.Composed registry
(qwen3_7 loads with zero engine edits; narrow fallback kept for four
historical ids the registry doesn't cover). model/builtin blank-imports
composed so serve binaries carry the registration. ChatML declared
config-driven (qwen-prefix predicate) through ChatTemplateDeclarer —
declared template beats tokenizer detection (precedence golden), gemma
fallback untouched. 3251 tests green in-branch; the composed live-serve
path needs a metallib runtime smoke on this box (bridge is thin
plumbing over composed's tested primitives).

Co-Authored-By: Virgil <virgil@lethean.io>
…l parity mechanism

Co-Authored-By: Virgil <virgil@lethean.io>
…ill chunks — e2b pp8K 3190->6777 tok/s (#381)

The mlx prefill-parity mechanism, ported: on gemma4 E-family, the trailing
20-of-35 KV-shared layers own no cache rows, and a non-final chunk's outputs
feed only that chunk's unread boundary hidden — so their compute is dead on
every chunk except the last (mlx-lm's per-chunk cache-state eval lets lazy
DCE prune exactly these layers; #381 traced its 2-3x prefill lead to this).

The port: sharedLayerSuffixStart validates a clean non-owner suffix at state
build (interleaved shapes disable the skip); prefillRetainedTokensBatchedDenseChunks
arms prefillSkipToLayer for non-final chunks only; the batched pass bounds its
layer loop there. Owner layers still land every KV row; the final chunk and all
decode/verify passes run the full stack. Token-identical by construction and by
receipt. LTHN_PREFILL_SKIP_SHARED=0 restores full-stack chunks.

Receipts (e2b 4bit, M3 Ultra, build/dist metallib):
  pp8K  (7225 tok):  3190 -> 6777 tok/s  (wall 2.265s -> 1.066s, 2.12x)
  pp62K (56712 tok): 2323 -> 5833 tok/s  (wall 24.41s -> 9.72s,  2.51x)
  32-token greedy continuations byte-identical skip-on vs skip-off at both depths.
  TestArchSessionPrefillChunksSkipSharedSuffix: serial-vs-chunked byte identity
  over a kv-shared fixture, both lanes, plus next-token step.

v1 scope: token-ids chunk lane only (the embeddings/bidir chunk lane keeps the
full stack); the PLE slab is still gathered full-width per chunk — both are
follow-up cuts. Chunk-width retune under the new balance is open (#381).

Co-Authored-By: Virgil <virgil@lethean.io>
…uard (#379)

composedEngineSession is the registry bridge for composed (hybrid-stack)
archs — "composed" names an arch class, like Arch, not a model, so it
meets the guard's neutrality bar. The full metal_runtime sweep is green
again with this entry.

Co-Authored-By: Virgil <virgil@lethean.io>
… bank the stale-metallib trap

Co-Authored-By: Virgil <virgil@lethean.io>
…ip — e2b pp8K 6777->8049 tok/s (#381)

With the skip armed, only the FINAL chunk pays the full layer stack — but
the absorb policy was handing it up to wide+w/2 rows (1081 at pp8K) when
only its LAST row's hidden is ever read. batchedDensePrefillChunkLenSkip
splits a minimal window-aligned boundary chunk off the end instead: the
last partial window, raised to batchedDenseSkipFinalFloor (32 — above the
recorded-ICB decline at 16 and at the q8 fold gate, so no lane falls back).
pp8K's full-stack span drops 1081 -> 57 rows.

Receipts (e2b 4bit, on top of 473c242's skip):
  pp8K:  6777 -> 8049 tok/s (wall 1.066s -> 0.898s; 3190 pre-campaign = 2.52x)
  pp32K: 7008 -> 7195   pp62K: 5833 -> 6040   pp118K: 4249 -> 4305
  32-token greedy continuations byte-identical skip-on vs skip-off at 8K + 62K.
  Full metal_runtime sweep green. Chunk-width sweep re-receipted under the
  skip: 2048 stays the peak (1024: 6453, 3072: 6402, 4096: 4986 at 8K).

Co-Authored-By: Virgil <virgil@lethean.io>
…t gather — e2b pp8K 8049->8528 tok/s (#381)

Two cuts to the per-chunk PLE tower build, the largest host-side span of a
skipped-chunk prefill (104ms of an 8K wall):

- lthn_ple_gather_rows_quant: ONE dispatch dequant-gathers all K rows
  (the K-loop of per-token gathers paid encode+launch per token). The bf16
  twin gains lthn_ple_gather_rows_bf16_pfx with the row STRIDE split from
  the gathered width. Old kernels stay — a stale metallib falls back to
  the loop / full width.
- Bounded slab: a skipped chunk (#381) reads only the owner layers' gate
  slices, so pleSlabFor now allocates the layer-major prefix and the batch
  builders derive the bound from the slab length (closures unchanged) —
  gather width, projection rows (a prefix of projW), elementwise, relayout
  and both copies all run at 15/35 of the width. When the bounded qmm_t
  instantiation is missing (QAT) the builder computes full width and
  copies the prefix — identical bytes either way. Sub-span instruments
  (pleSlab.ensure/stage/gpu/out) stay in.

Receipts (e2b 4bit, on top of c257ff0):
  pleSlab host span @8K: 104.1ms -> 47.1ms steady-state (gpu 37.1 / out 4.1 / stage 3.5)
  pp8K:  8049 -> 8528 tok/s (wall 0.847s)   pp32K: 7195 -> 7556
  pp62K: 6040 -> 6285                        pp118K: 4305 -> 4437
  32-token greedy continuations byte-identical skip-on vs skip-off at 8K + 62K.
  Full metal_runtime sweep green (PLE parity suite exercises the new gather).

Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Virgil <virgil@lethean.io>
…blocks the host (#381)

The quant PLE builder committed its command buffer and WAITED so the host
could copy the slab out — which the pass then uploaded straight back. Both
sides of that round-trip are dead: the pass's own command buffer follows on
the SAME queue, so the GPU orders the read after the build with no host
involvement at all.

perLayerInputsBatchQuantEncode is the shared core (encode + commit, optional
wait); IntoSlab keeps the wait+copy contract for existing callers, the new
perLayerInputsBatchQuantDevice returns the layer-major tensor's buffer
committed-not-waited at exactly the chunk's layer bound. pleSlabForPrefill
resolves device-first; the pass binds prefillPLESlabDevice directly instead
of pleSlabBuffer(hostSlab). Single-buffered scratch stays race-free: the
host stages the next chunk only after the pass's wait, which covers the
builder's buffer too. Host-slab path unchanged for QAT-without-bounded-qmm,
small K, and stale metallibs.

Receipts (e2b 4bit, on top of a0364b5):
  pleSlab host span @8K: 47.1ms -> 3.1ms (encode-only; wait/copy-out/upload gone)
  pp8K:  8528 -> 8708 tok/s (wall 0.830s)   pp32K: 7556 -> 7681
  pp62K: 6285 -> 6359                        pp118K: 4437 -> 4469
  32-token greedy continuations byte-identical skip-on vs skip-off at 8K + 62K.
  Full metal_runtime sweep green.

Co-Authored-By: Virgil <virgil@lethean.io>
…op dies, e2b pp8K 8708->9016 tok/s (#381)

The last host-side per-token work in a prefill chunk was the embedding
dequant loop (embedInto per token — 17.6ms of an 8K wall). The quant PLE
builder's command buffer now gathers the K main-embed rows too (the same
rows kernel, dModel width), the projection reads them in place of
host-staged hidden rows, and the batched pass takes the same buffer as its
input rows (directInputRows + i·rowBytes offsets) — only the token ids ever
cross to the GPU. perLayerInputBatchDevice drops the embs parameter
entirely; prefillInputsDevice is the one-call device-first seam, and every
fallback (bf16 sessions, stale metallib, small K, QAT-without-bounded-qmm)
keeps the host embed loop + host slab unchanged.

Receipts (e2b 4bit, on top of cfc84d5):
  embed host span @8K: 17.6ms -> 0 (inputsDev 0.8ms encode-only)
  pp8K:  8708 -> 9016 tok/s (wall 0.801s)   pp32K: 7681 -> 7849
  pp62K: 6359 -> 6474                        pp118K: 4469 -> 4522
  32-token greedy continuations byte-identical skip-on vs skip-off at 8K +
  62K, and byte-identical to the pre-gather build's output.
  Full metal_runtime sweep green.

Night cumulative: pp8K 3,190 -> 9,016 (2.83x); mlx-lm true-wall gap 1.11x.

Co-Authored-By: Virgil <virgil@lethean.io>
…2.83x (#381)

Co-Authored-By: Virgil <virgil@lethean.io>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3.9% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

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