Conversation
… 2.11.2 MTPLX 2.11.2 re-landed nine of PR 391's twelve Flash-Next decode keys; this ports three of the unlanded five onto upstream's re-landed structure, wired into the fixed-M4 auto-arm block with the MTPLX_QWEN4_*/MTPLX_QSA_* namespace and a per-key =0 opt-out (the old MTPLX_FABLE_* names kept as aliases). - HC_M4 (MTPLX_QWEN4_HC_M4): the verify-width (2..8 row) hyper-connection read run as one multi-threadgroup GEMV (kernels/qwen4_m4_hyper_read). Reader in runtime_options read once at import; GatedResidual gains the geometry eligibility check, pack validation and the fused read; install validation runs after the M4-stage3 install and reports at /health qwen4_install_reports.hc_m4. Rounding-class. - prefill causal-mask fuse (MTPLX_QWEN4_PREFILL_MASK_FUSE): the dense QSA prefill chunk goes through MLX's fused SDPA instead of a materialized score tensor MLX's head-dim-256 heuristic declines; a per-shape-class capability cache keeps a verify step MLX refuses from disarming a wide chunk. Rounding-class (exact visible set). - QSA prefill query tile (MTPLX_QSA_PREFILL_QUERY_TILE): tiles only the dense QSA attention query rows so a wider prefill chunk keeps the narrow chunk's attention peak and cost. Value companion, default 2048 (inert at the production 2,048 chunk width). Rounding-class (exact visible set). Each is default-on for a served fixed-M4 Flash-Next pack (server auto-arm lane_defaults, gated on the fixed-M4 config predicate) with a per-key kill switch through the existing pop loop, and registered in the boot-time runtime-env validator. All three are rounding-class, so quality-gated on HumanEval. Two of the five remain and are documented in docs/perf/qwen38-391-remainder.md: the QSA sparse split-K decode (a native kernel whose build and parity probe need the GPU) and the graph-build overlap (its prefix/suffix split of the fixed-M4 verify has no substrate on 2.11.2's single-graph verify). CPU tests (venv mlx 0.32.2, no GPU): tests/test_qwen4_hc_m4.py 53 passed, tests/test_qwen4_prefill_mask_fuse.py 40 passed.
The fourth of PR 391's five unlanded lanes: MTPLX_QSA_SPARSE_DECODE, the native split-K sparse-GQA attention for the M=4 fixed verify. It reads the selected KV rows of the fixed QSA cache once per verify cycle instead of materializing a gathered [1,2,4,2052,256] K/V pair per layer, which is where the shipped lane's bytes are. Rounding class: fp32 online softmax over the exact visible set. - kernels/qsa_sparse_decode.py + native_extensions/qsa_sparse_gqa (package mtplx_native_qsa: the split-K Metal kernel, steel headers and a nanobind binding); mtplx/native loads it, runtime_options reads MTPLX_QSA_SPARSE_DECODE (+_TILE 128:32, +_SPLITS 17); the old MTPLX_FABLE_* names are honoured as aliases when the new key is unset. - graphbank.TensorOffsetQSACache validates the lane ONCE at cache install (a real parity probe, outside any mx.compile trace); the twin re-promotion sites and the compiled verify_step carry it, and the verify body asserts the lane is in the traced graph. models/qwen4_exp routes the fixed-capacity verify width to the kernel (QSAIndexer._sparse_decode_route) or declines to stock for a request shape it cannot serve. - Server auto-arm: default ON for the fixed-M4 pack ONLY when the native extension is built; a wheel without mtplx_native_qsa declines to stock with a logged verdict and still serves. An explicit MTPLX_QSA_SPARSE_DECODE=1 reaches the fail-closed install (armed and unbuilt raises). Registered in the boot-time runtime-env validator; kill switch through the existing pop loop. - scripts/bundle_native_runtime_wheel.py signs and packages mtplx_native_qsa alongside mtplx_qsa_kernels (Developer ID, hardened runtime, secure timestamp), with tests. - The mask-fuse refusal test now accepts either MLX build's native wording: the lane logs a version-independent per-class line and never raises under default arming (it falls to the stock dense SDPA). Load-time parity on stock mlx 0.32.2 with the native kernel built: vs the fp32 reference worst rel_l2 3.1e-05 with the top-1 token identical, vs the stock gather path rel_l2 4.6e-03 (rounding class), across the 4093 and 2052 probe cells that stand in for the 16K and 261,120 serving regimes. CPU tests (venv mlx 0.32.2): tests/test_qsa_sparse_decode.py, tests/test_qsa_sparse_decode_wiring.py, tests/test_qsa_sparse_gqa_native.py and tests/test_bundle_native_runtime_wheel.py all green.
Served via `mtplx serve` (cli-resolved Turbo, no lane flags), the QSA split-K decode lane did not engage: qsa_sparse_decode_enabled() read the environment at IMPORT and cached the default (False), but the fixed-M4 auto-arm stamps MTPLX_QSA_SPARSE_DECODE (native-gated) into the environment AFTER runtime_options is imported, so the cache froze the default before the stamp landed -- the lane was absent from /health with neither an "armed:" nor a "declined to stock" line. hc_m4 escaped only because its reader is read on a path where the module was imported after the stamp. Resolve the flag lazily on the FIRST read (which is the graphbank cache install, after the overrides are applied), then cache; the _QSA_SPARSE_DECODE module global stays (tests force it to a bool) and the native-gated default in the server auto-arm is unchanged. The env is frozen once serving starts, so a lazy first read is still a single cached bool on the hot path. Regression tests, the shape that would have caught this: - the reader picks up a stamp applied AFTER import (an import-frozen reader fails it), - the fixed-M4 auto-arm block stamps the lane when the native extension is built, or prints the declined-to-stock verdict and leaves it unstamped when it is not.
Second arming failure (battery, 2026-09-07): served as `mtplx serve` launches it, hc_m4 was OFF for the same reason the QSA decode lane was in commit 3 -- its reader froze the environment at import (default off) while the fixed-M4 auto-arm stamps the lane keys into the environment AFTER runtime_options is imported. The earlier claim that hc_m4 read on a post-stamp path did not hold for the served path. Resolve every remainder-lane flag at USE (the install / route path, which runs after the overrides are applied), never at import: - runtime_options: qwen4_hc_m4_enabled, qsa_sparse_decode_tile and qsa_sparse_decode_splits (qsa_sparse_decode_enabled was fixed in commit 3); each keeps its module global as a test override (None = read env). - models/qwen4_exp: _prefill_mask_fuse_enabled drops @lru_cache (its body already reads os.environ), so a stamp landing after import is seen. - qwen4_prefill_chunk.resolve_query_tile_rows already read at use. The native-gated default and the MTPLX_FABLE_* aliases are unchanged. Upstream's own MTPLX_QWEN4_OPDIET / MTPLX_QWEN4_VERIFY_GLUE readers are left as-is (not part of this remainder set). Test reproducing the served order (tests/test_qwen4_remainder_arming.py): import mtplx.runtime + mtplx.server.openai FIRST, assert all four readers off, run _server_runtime_env_overrides for the fixed-M4 pack, apply it to os.environ, then assert all four arm -- the decode lane armed when the native extension is built, else an explicit declined-to-stock verdict, never silent absence. The hc_m4 read-once test is rewritten to assert read-at-use, and the mask-fuse test drops its now-defunct cache_clear() calls.
Arming audit (battery, 2026-09-07): `mtplx serve` imports generation / runtime / model modules before parse_args stamps the auto-arm env, so any flag whose reader resolves at module import freezes its default before the stamp lands and the auto-arm's setdefault is a silent no-op as launched. A column-0 scan of every module holding a stamped key's reader found exactly four such readers among the ~31 auto-armed keys; every other stamped key reads the environment at use or is consumed from config.json at model load. All four are decode-verify lanes, so the release control (71.17 tok/s at 16K) ran without them -- a plausible slice of the 71->81 gap. Resolve all four at use (read the environment each call; the module global stays a test/force override; the env is frozen once serving starts, so two traces of one graph still read the same value): - MTPLX_QWEN4_DRAFT_K20_PRESCATTER: qwen4_draft_k20_prescatter._ENABLED, and generation.py's cached _QWEN4_DRAFT_K20_PRESCATTER (removed; the one draft consult site calls the reader). - MTPLX_QWEN4_BLOCK_VERIFY: qwen4_block_verify._ENABLED, and generation.py's cached _QWEN4_BLOCK_VERIFY (removed; the accept-loop consult calls the reader). - MTPLX_QWEN4_OPDIET (+ _ITEMS): runtime_options. - MTPLX_QWEN4_VERIFY_GLUE (+ _ITEMS): runtime_options (reset hook kept, now forcing the globals). No default value changed; keys that already read at use are untouched. Upstream's STRICT_CLAIMS and BATCH_PAGED_OFFSETS are also import-frozen but are not auto-armed (operator sets them pre-launch), so they are left as-is. Tests: tests/test_qwen4_remainder_arming.py extended to assert all four arm in the served order (import first, stamp, read) and that the fixed-M4 auto-arm stamps OPDIET / BLOCK_VERIFY / VERIFY_GLUE and their readers then arm. The block-verify and draft-k20 source-inspection tests and the opdiet read-once test are rewritten to assert read-at-use.
The arming audit's lesson: gate on the install verdict, not the env. Three
decode-verify lanes had no per-window observable in
/health qwen4_install_reports -- draft_k20_prescatter, block_verify, opdiet --
so a served window could not confirm they engaged. Add read-only reports (no
behaviour change, no defaults touched):
- draft_k20_prescatter: {armed (read at use), engaged (first-use latch set when
claim_draft_route installs the route), receipt (the last install receipt)}.
- block_verify: {armed, engaged (latched when a block verifier is built for the
accept loop)} plus a one-shot "[mtplx] MTPLX_QWEN4_BLOCK_VERIFY armed:" log.
- opdiet: {armed, items (configured selection), applied (first-use latch of the
items that actually ran at a gated site)}.
Each appears only when ARMED (read at use, gate-able without a request), so an
unarmed lane stays absent (== off) like the other lanes; the engaged/applied
latch rides inside the armed report.
CPU test: tests/test_qwen4_remainder_arming.py asserts the three reports are
absent when off / =0 and present with armed True under a served-order stamp.
… 391 remainder port Rebase of PR youssofal#475 (cached async PLE + pooled-key rowsel) onto the PR 391 remainder port head (perf/qwen38-391-remainder-main = upstream main 2.11.2 + the four remainder lanes: HC_M4, prefill mask fuse, QSA query tile, and the QSA split-K sparse-GQA decode extension. The two lanes here add to the same fixed-M4 lane_defaults / _QWEN4_PORT_KEYS block, and their native loader (ple_cpu_rows) and wheel-signer entry union with the QSA sparse-decode extension's (mtplx_native_qsa), all cleanly additive). PR 391 is closed and its Fable delivery stack (mtplx/full_stack_env.py, the turbo-full-stack profile, mtplx/native's QSA sparse-GQA loader) was never merged, but the maintainer independently re-landed the Flash-Next stack under the MTPLX_QWEN4_*/MTPLX_QSA_* namespace, so both lanes' base contracts are present upstream and both apply: the fixed-M4 compiled-verify auxiliary plane (mtplx/qwen4_fixed_verify.py) and the QSA indexer pooled-key kernel (mtplx/kernels/qsa_indexer_prepare._pool_keys_kernel). The work here is re-siting the arming and native loading off the absent full_stack_env onto upstream's own machinery. Lanes (armed by default for a served fixed-M4 Flash-Next pack, per-lane opt-out): - ple_cached_aux: a native CPU-stream provider stages the fixed 64 M4 n-gram rows and the auxiliary embedding plane is produced with mx.async_eval outside the compiled verifier. The stock owner-side row cache is preserved; declines to stock with a printed reason when the ple_cpu_rows extension is not built. - qsa_pooled_rowsel: the twelve QSA indexers' pooled-key preparation binds the pool kernel metadata once per indexer and shares one inv_freq object. Rebase changes vs the closed-PR commit: - mtplx/qwen4_aux_lanes.py rewritten off full_stack_env: primary keys are MTPLX_QWEN4_PLE_CACHED_AUX / MTPLX_QSA_POOLED_ROWSEL, the PR 391 MTPLX_FABLE_* names kept as aliases (primary wins when both set). - mtplx/server/openai.py: the two keys join the fixed-M4 lane_defaults and _QWEN4_PORT_KEYS (so the existing pop-loop kill-switch honours KEY=0), with an alias pre-step mirroring an operator's MTPLX_FABLE_* export onto the primary. - mtplx/qsa_pooled_rowsel.py: op-diet contract re-pointed from the absent fable_opdiet_enabled to upstream's qwen4_opdiet_enabled (MTPLX_QWEN4_OPDIET). - mtplx/runtime.py: the two installs run after the fixed-M4 verify install, logging instead of the removed _print_install_receipt. - mtplx/profiles.py: the two keys added to MODEL_RUNTIME_ENV_OVERRIDE_KEYS so normalize_runtime_env_overrides accepts the server-stamped values. - mtplx/native/__init__.py: a minimal PLE-only loader (load_ple_cpu_rows_extension / ple_cpu_rows_unavailable_reason); PR 391's qsa_sparse_gqa loader is not reproduced (upstream loads native QSA via kernels/qsa_prefill_direct.py). - scripts/bundle_native_runtime_wheel.py: accepts and Developer-ID signs the new mtplx_native_ple_cpu_rows extension alongside mtplx_qsa_kernels, so a notarized release wheel carries a signed ple_cpu_rows Mach-O. - scripts/fable/setup_over100_venv.sh: builds only ple_cpu_rows. 772f5be's opt-in interleaved n-gram row cache (MTPLX_NGRAM_ROW_FILE, default off) touches the same _SidecarGather rows but at the disk-layout layer; it is orthogonal to this runtime-scheduling lane and does not subsume it. CPU tests (venv mlx 0.32.2, no GPU): 84 passed across the six lane test files plus the wheel-bundler test.
… add the 16K interleave chart
…senses of "exact" Against pr-bodies/GLOSSARY.md, the same pass the pull request bodies had. The arm table gave an acceptance mode for D and E only, so A and C read as though they had none. Both run with typical off, and their rows now say so. "Exact" appears once here, in the optimization-class sense: an exact optimization is byte-for-byte the stock path, against a rounding-class one that differs only by floating-point rounding. Nothing in the file bound that, so it was indistinguishable from the exact acceptance LAW that youssofal#478 and youssofal#485 use the same word for. The paragraph now binds it and states that it never labels an arm, since arms A and C are rounding-class builds overall. The prose mixed short context labels with exact counts; prose now gives the counts and the short form is left to table row labels.
…verflow The long-context verdict table blamed a QSA-indexer prefill transient for the 261,120-token OOM on all four arms. The prefill completes on every arm; the overflow is the first decode step's speculative-verify KV-cache write, where TensorOffsetKVCache.update_and_fetch used the functional mx.slice_update and reallocated the full per-layer KV buffers (about 6.4 GB in one command buffer). The probes W1 and W2 targeted prefill and so could not have fit. This matches the youssofal#475 body's Section 1.5 and the youssofal#482 fix arm, which fits 261,120 on all three cold seeds at 100.82 GB.
Contributor
Author
|
It should be noted that there are several fixes that need to go in and this is just one. |
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.
This AR change is stacked on MTPLX #475, whose tested parent head is
cd6023ecf5b78bfd75733fae0b0ad8004161e20b. This upstream PR targetsmain, so GitHub also displays the parent changes while #475 remains open. Review the focused AR child PR or its AR-only compare for the actual new diff. After #475 merges, this PR can be rebased on upstreammainto show only the AR change.The problem
The existing sampled-AR pipeline overlaps host graph construction by making the next token part of the lazy MLX graph. That removes a host bubble, but it also replaces the request's established sampler:
_sample_from_logits, applies MTPLX's existing filtering order, and draws from the request'snumpy.random.Generator;_mx_lazy_sampleand drew withmx.random.categorical.The same request and seed therefore produced a different continuation. A regression test catches the divergence at token 3 under temperature 1 / top-p 0.95 sampling. This is a correctness failure even though both streams are individually deterministic.
The pipeline also required a resident 32 GB n-gram table. On the 128 GB Qwen3.8 serving configuration, the PLE table is streamed from its SafeTensors sidecar, so constructing a forward on an unknown token would otherwise force a host synchronization.
The resulting schedule
This PR keeps the original sampler and overlaps only work that is independent of its result:
For every decode step:
int64[1]token leaf initialized to zero._sample_from_logitswith the same NumPy generator used by ordinary AR.The model still dequantizes the same packed sidecar bytes through the ordinary MLX operations. There is no alternate arithmetic, token prediction, speculative rollback, or cache repair.
Construction-time contract
The optimized lane resolves its invariants once when it is installed or activated:
flush/discardcallable are prebound;The existing request-level
MTPLX_AR_PIPELINEeligibility gate remains unchanged. If the model cannot install the complete route, it declines before entering the pipeline and uses the explicit classic AR route.The public stats envelope now includes
ar_pipeline_activeandar_pipeline_variant, allowing a benchmark to prove thatvariant=streamedactually ran.Correctness gates
The focused test battery covers:
Results:
The full-model parity gate used the unchanged #475 head as control and final commit
775697bb40a8a19c8ecd06cea73b32cd3ae5a8a0as candidate:MTPLX_AR_PIPELINE=0on #475 control,=1on candidateAll three pairs matched on request-body hash, prompt hash, completion count, finish reason, visible-text hash, and reasoning-text hash. Every candidate response reported
ar_pipeline_active=trueandar_pipeline_variant=streamed.That is a 13.97% median decode-throughput gain with the same 85.08 GB peak-memory reading.
Sustained A/B/A result
The final performance confirmation used the same artifact, prompt, sampler, reasoning mode, seeds, cache policy, fan policy, and 40 C start gate, with a 1,024-token output cap. It ran control A, candidate B, then control A2. Seeds 20260829 and 20260830 reached 1,024 tokens; seed 20260831 emitted the same stop at token 666 in all three arms.
Across the six surrounding control samples, the candidate improves median decode throughput by 13.47% and reduces median end-to-end wall time by 8.26%. Prefill, TTFT, and peak memory are unchanged within run noise. All A/B/A output hashes match for every seed, and all candidate records prove the streamed route was active.
Scope
This changes only the Qwen4/Qwen3.8 sampled target-AR lane layered on #475's native cached-PLE extension. MTP verification, draft sampling, greedy AR, constrained decoding, penalty-bearing sampling, prefill arithmetic, and default-off behavior are unchanged.