Repository navigation
[Metal] thread-scoped command-buffer commit limits - #36
Merged
Merged
Conversation
Add mlx_metal_set_thread_command_buffer_limits(max_ops, max_mb, ...): the calling thread's command buffers commit after max_ops operations or max_mb megabytes of distinct inputs instead of the device defaults (zero keeps a default; the previous values are returned for restoring). Evaluation encodes on the thread that calls eval or async_eval, so a caller can give decode steps larger buffers (the CPU encodes further ahead of the GPU) while prefill keeps the defaults and their memory bound. The defaults (50 ops / 50 MB on Ultra parts) count weight inputs, so multi-GB expert banks commit after almost every MoE operation. Partitioning work into command buffers does not change results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 16, 2026
Declare mlx_metal_set_thread_command_buffer_limits with MLX_API, as the sibling gemma4 entry points are, so a shared libmlx built with hidden visibility exports it. Include the header from device.cpp only, and keep the C++ helper in the file's anonymous namespace.
Gajesh2007
added a commit
to Layr-Labs/mlx-swift
that referenced
this pull request
Oct 21, 2026
Source/Cmlx/mlx moves from the PR branch tip 5e28974c to the squash-merged main commit 71271f08, which carries the same change plus the MLX_API export of mlx_metal_set_thread_command_buffer_limits.
Gajesh2007
pushed a commit
to Layr-Labs/mlx-swift
that referenced
this pull request
Oct 21, 2026
Exposes the fork's thread-scoped Metal command-buffer commit limits (Layr-Labs/mlx#36) to Swift as GPU.withCommandBufferLimits(_:_:) with GPU.CommandBufferLimits: command buffers encoded on the calling thread commit after the given operation count or megabytes of distinct inputs and outputs instead of the device defaults; the previous limits are restored on exit (also when the body throws). Results do not depend on the partitioning. Adds the C header Source/Cmlx/include/mlx/command_buffer_limits.h, pins Source/Cmlx/mlx to the merged mlx#36 (71271f08), and covers the API with CommandBufferLimitsTests (nesting, throwing body, negative values, thread isolation, identical results). fork.yaml describes the change.
Merged
4 tasks done
Gajesh2007
added a commit
to Layr-Labs/d-inference
that referenced
this pull request
Oct 21, 2026
Brings in master through 927172b (#1394, #1524) and the merged-dependency pins through the parent: mlx 71271f08 (Layr-Labs/mlx#36) and mlx-swift 81f58d79 (Layr-Labs/mlx-swift#57). The libs/mlx-swift-lm conflict resolves to 8df7bb62, the squash-merged Layr-Labs/mlx-swift-lm#317 on main (same tree as the SDK this branch's fixes were tested against, a00f4730).
Member
|
Merge-readiness review (independent reviewer agents + local validation) before merging:
|
Merged
9 tasks done
Gajesh2007
added a commit
to Layr-Labs/d-inference
that referenced
this pull request
Oct 21, 2026
…ion slot (#1517) * refactor(nemotron-super): open the pinned checkpoint in ProviderCore NemotronSuperCheckpoint reads the pinned index, template, configuration facts and shard set, then streams every shard against its pin before any tensor header is parsed. The qualification runner now uses it instead of its own copy, and the managed provider load can share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): owner serves its slot's model id under an admission floor The load transaction, builder, bindings and request plan take the model id they serve (the qualification hook keeps its Hugging Face id), and the LOAD claim's KV headroom can be smaller than the grant, which stays the engine's admission ceiling. Staging is a seam: the pinned checkpoint (config identity, index and shard pins) in production; fixtures stage through the same claim, fences and builder. The chat route exposes one engine per raw body with media optionally refused, partitions text prefix reuse by a per-request cache scope, refuses logit_bias it cannot honor, and reports the reference prompt length for usage floors. NemotronSuperServingPolicy holds the production identity, defaults and load plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): production discovery and a managed provider slot Discovery advertises exactly nvidia-nemotron-3.5-super with the canonical nemotron_h_omni declaration and the pinned config and index; the registry id with other metadata is withheld and every other id of the architecture stays unsupported, as on providers without this change. Its ModelInfo carries the managed LOAD footprint (installation and head claim, prefill lane, admission KV) and no media. ensureModelLoaded hands the load to ProviderLoop+NemotronSuper: the grant comes from the ordinary re-slice held under the re-slice gate until installation, NemotronSuperLocalLoad claims LOAD and builds the owner (depth-3 rectangular MTP and eight requests by default; mtp_mode, the kill switch and an explicit engine_v2_max_concurrent change them; the plain owner reuses exact text prefixes when the grant holds the bank), and the slot retires only through the owner. Generic factories, the standalone server, warm rebuilds and the generic local engine refuse Super. Coordinator and unified local-endpoint requests run on the owner's chat route: pinned prompt contract, reasoning, tools, structured output, cancellation and usage, with prefix reuse partitioned by the authenticated scope and no cache receipts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): gated full-checkpoint run of the managed slot DARKBLOOM_SUPER_MANAGED_LIVE=1 with NEMOTRON_SUPER_CHECKPOINT_DIR loads the pinned checkpoint through the production loader and serves one encrypted coordinator request; the capacity quote case checks cold quotes against the managed LOAD footprint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(nemotron-super): managed slot, discovery, RAM floor and registration Inference describes the managed slot and its load plan; the registry pages describe exact-identity discovery and the registration contract; hardware pages derive the 192 GB floor from the default LOAD footprint; configuration, direct mode and test pages cover the controls, local endpoint and suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(libs): pin mlx-swift-lm at the Super production head ed14432e The SDK makes NemotronSuperCheckpointPin public so the managed load opens every shard against the same pins NemotronSuperInstallation stages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(nemotron-super): apply the production refactor review The managed load releases the re-slice gate at one success and one failure site, the generic re-slice shares the shrink, restore and grow helpers, and an identity helper replaces repeated checks. A load that retains a native fault no longer restores survivors' larger grants while its weights stay resident. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): serve images on the speculating owner The owner's image binding no longer refuses work when text speculation is installed: the engine runs each image request target-only, with no draft or assistant state, while text keeps speculating. The slot routes inline JPEG/PNG requests from the coordinator and the local endpoint to that binding and is marked as serving images. Tests cover image output equal to the plain owner's, text that stays token-exact beside an image, coordinator and local image requests on the managed slot, an image arriving mid-stream beside speculating text, and a media reservation that does not fit refused before allocation. A gated full-checkpoint test asks PNG and JPEG questions and runs one beside text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): advertise image input with its media charge Advertise image input for nemotron_h_omni only under the exact registry id nvidia-nemotron-3.5-super; every other id, alias or spelling stays text-only, and discovery still withholds the id without its pinned config and index. The id now lives in ModelMediaPolicy and the serving policy references it. The serving policy states the image binding's worst-case reservation for one encoded request with 1...16 images: ingress at the 8 MiB body and the pinned template, each image's decoder workspace at the 64 Mi pixel ceiling, both copies of the decoded body, visual tokens up to the 16,384 context and one largest-grid (13,312 patches) tower and projector activation. The discovery footprint adds the single-image charge (3,888,385,020 bytes), so estimated_memory_gb becomes 126.32 GiB; native_load_transient_bytes is unchanged, as media is not LOAD. Capacity quotes for Super vision probes charge that reservation: cold probes against free_for_load_gb beyond the footprint's single image, resident probes against load_usable_gb net of load_headroom_gb, and more than 16 images is a capability reject. Text probes and other models keep their quotes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(nemotron-super): images on the managed slot and their memory Inline JPEG and PNG images are served on the slot, target-only on the speculating owner; the registry input, direct-mode controls, discovery and changelog say so. The discovery footprint now includes one image request's media charge (126.3 GiB); the RAM derivation is updated and 192 GB still holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): advertise forced tool_choice for the registry id The Super owner prompt-forces required and named tool calls on text requests and withholds output until the call validates, so the provider lists `nvidia-nemotron-3.5-super` in `tool_constraint_models`. Image requests still refuse tools. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(libs): pin mlx-swift-lm at the Super image head c1879c2e Pins the SDK head whose Super MTP targets serve image requests target-only, and lists its new test file in the SDK harness template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): check streamed image responses for leaked media SSE frames start with `data:`, so the hook test now checks that no base64 media reaches a streamed or JSON image response instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(nemotron-super): list the image tests of the managed slot Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): opt-in paged attention KV for the Super owner DARKBLOOM_NEMOTRON_SUPER_KV_BACKEND=paged builds the owner's engine on the segmented native paged backend (explicit: a paged construction failure fails the load). The engine's process-memory owner then charges pages and recurrent rows and the bridge reserves no request bytes, so the owner reserves every configured request's decode-row workspace from before the first request until native retirement, like the prefill lane. The prefix bank lives in the contiguous engine: an explicit opt-in with paged fails the load and the managed plan keeps its default bank off. Tests cover the switch, a paged owner serving concurrent text and images with every charge retired and text equal to contiguous, paged speculation equal to plain paged text, and the prefix refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(nemotron-super): paged KV switch, runner charge rows and harness lists The runner reports the paged owner's decode rows as decode_rows and counts them among held request charges; the harness templates list the paged SDK and provider suites; configuration, architecture and runner docs and the changelog fragment describe the switch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): exact text prefix reuse on the speculating owner The default production owner (depth-3 exact rectangular speculation) now gets the same resident checkpoint bank as the plain owner: the MTP target is built with `exactPrefixCheckpoints` and the SDK assistant checkpoints its queued prompt history beside the target state, so a restored request drafts from the same history as a cold one. `loadPlan` turns `DARKBLOOM_PREFIX_CACHE_MEMORY` on for either owner when the grant holds every request's fixed bytes beside the bank (an operator value or kill switch still wins); the eight-request default at a 100 GiB grant now claims 27 GiB of KV headroom including the 1 GiB bank. `NemotronSuperLocalPrefixCache.config` no longer takes `speculates`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): prefix reuse on the speculating owner The provider prefix test now also runs a speculating owner (depth 3, exact rectangular): restored text equals a second speculating owner's cold text and both owners hold their lane and bank until retirement. The SDK harness template lists the speculative prefix suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(libs): pin mlx-swift-lm at the speculative prefix head 05a81d81 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(nemotron-super): retire an unpublished paged owner when its decode rows cannot be sized Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(nemotron-super): fail closed on non-finite image features The owner checks the evaluated tower and projector features before submission; a non-finite value fails the request instead of decoding as a stream of `<unk>`. A regression test decodes one ImageIO picture as PNG and as JPEG and checks that both normalize alike and stay inside the reference normalization's range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(nemotron-super): re-sample memory while an unloaded slot's pages return On the full checkpoint macOS returns about 40 GiB of an unloaded Super slot's pages up to a second after Metal drops them, so a reload right after an unload was refused. A Super retirement now opens the bounded two-second re-sample window that a Qwen4 retirement already uses; a load in that window samples again instead of refusing. The gate itself is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): reload, image concurrency, video, forced tools, live runs Fixture tests: unload releases the owner, engine, model and head and the slot reloads while the OS still withholds the unloaded pages; a second concurrent image gets 429 and video gets 400 with nothing held; forced tool_choice validates required and named calls and refuses plain text. Gated full-checkpoint tests move to their own file and add a greedy text identity run for comparing builds, a reload run and an image matrix (sizes, formats, content check, concurrency, history, reasoning). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(nemotron-super): apply the image refactor review Comments and docs that still said a speculating owner refuses images now describe target-only image requests; the managed route's signature is current; the Super model type is one constant beside its id. The docs record the saturating activation-overflow policy and the reload window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): exact prefix reuse of image turns With the owner's checkpoint bank and a cache scope, an image request is bound to an identity of its evaluated features and spans (`PreparedSubmission.hybridPrefixIdentity`), hashed while its media charge still covers the copy, and submitted with prefix caching on in that scope: a later turn with the same images at the same positions restores the deepest chunk checkpoint, on the speculating owner target-only. The HTTP hook and managed slot pass the request's cache scope to the image binding as they do for text; without a scope an image request stays cold and never publishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): image turn prefix reuse on both owners The provider prefix test runs a direct image turn on the plain and on the speculating owner: the next turn with the same image restores the deepest chunk boundary and streams the cold owner's text, and an image request without a cache scope neither restores nor publishes. The SDK harness template lists the bound-image suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(nemotron-super): apply the speculative prefix refactor review Bank and test docs describe image turns and the shared bank of both owners. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(libs): pin mlx-swift-lm at the speculative prefix head c9cef667 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(nemotron-super): direct mode reuses prefixes of image turns Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): bound the live reasoning image case; runner doc A debug build decodes about three tokens a second, so the reasoning-on image case keeps its output inside the request deadline. The runner doc no longer says the speculating hook refuses images. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(libs): pin mlx-swift-lm at the Super saturating head 9930b553 Pins the SDK head whose Super installation and MTP head quantize activations with saturating overflow, plus the media-fallback refactor review, and lists the new SDK test files in the harness template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): drop the PNG/JPEG decode diagnostic print Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(nemotron-super): cite the SDK doc by its submodule path Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): run the generic checkpoint suites in the SDK harness The prefix bank's causal-media and durable-store gates change shared CBv2 code, so the Super SDK harness template also lists the hybrid media identity/lifecycle and complete-checkpoint suites that cover it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): gated full-checkpoint paged logit comparison NemotronSuperPagedLiveTests stages the pinned checkpoint once and runs one 16,000-token prompt in 2,048-token chunks and 64 decode steps on contiguous and paged attention KV, plus a contiguous arm prefilled in 1,024-token chunks that calibrates the trunk's sensitivity to attention reduction order. Paged prefill must be bitwise equal; paged decode must stay within the larger of 0.1 and twice the calibration arm's deviation. Gated by DARKBLOOM_SUPER_PAGED_LIVE=1 and NEMOTRON_SUPER_CHECKPOINT_DIR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): do not race the response tracker after a reset After the client resets the image stream, the transport may already have retired the response body, so the tracker can read 0 or 1. The test now asserts what the reset must not change: the native pump and its media and KV charges stay held until the terminal gate opens, and the tracker and charges reach zero after retirement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): size the prefix bank from the KV grant The pinned checkpoint's owner sized its exact-prefix checkpoint bank at min(1 GiB, grant / 8), about two long prompts, however large the grant. NemotronSuperServingPolicy.defaultPrefixBankBytes now takes half of what the grant leaves beside every request's fixed bytes, the prefill lane bound and one image's media charge, at most a quarter of the grant (the engine's own ceiling after a re-slice) and 16 GiB, in 256 MiB steps, and no bank below 1 GiB. The managed slot's load plan and the builder size the same bank from the same grant, requests and keys; the plan turns the switch on only for a non-zero size and adds the bank to the LOAD claim, and the bank is carved from the grant and reserved until native retirement exactly as before. An operator size still wins, synthetic checkpoints keep the generic default, and the Super bank allows one entry per 256 MiB (at least 32). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(libs): pin mlx-swift-lm at the merged Super production head b5608a59 The SDK head merges the speculative prefix reuse line, the fused SSD fix and the engine's fail-closed check for non-finite sampled logits; the harness template lists the new logit tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): draft depth by decoding requests on the managed slot `DARKBLOOM_NEMOTRON_SUPER_MTP_DEPTH` also takes a comma list of up to eight depths 1...3: entry N for N requests decoding in the step, the last for more (rectangular verification only; the largest entry sizes admission). The managed slot's speculating owner now defaults to `3,2,2,2,1` instead of a fixed depth 3, keeping an operator's value. Measured on the full checkpoint with natural prompts (greedy, every output byte-identical to the plain owner's), aggregate tok/s for the plain owner and depths 1 / 2 / 3: one client 32.5 and 47.3 / 54.9 / 57.0, two 50.7 and 60.9 / 65.0 / 63.9, four 67.2 and 77.3 / 78.4 / 76.7, eight 82.8 and 86.9 / 84.7 / 80.5. These numbers need the SDK's cached recurrent spec (mlx-swift-lm claude/super-serving2); without it every depth served slower than plain decode. A new policy test pins the managed grant on the measured 256 GB Mac: a lone slot's re-slice grant is the whole fleet KV budget (above 150 GiB), which holds the speculating and plain admissions uncapped and the 25.9 GB minimum for eight speculative requests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): serving policy benchmark and runner engine counters The qualification runner gains `GET /runner/engine` (cumulative speculation counters and, with CBV2_STEP_PROFILE=1, step-profiler phase timings) and `POST /runner/engine/profile/reset`; its identity records the depth table; and its speculative KV default scales with the width like the managed slot's admission (26 GiB for eight requests) instead of a flat 12 GiB that refused MTP x 8. `bench_serving.py` serves 16 natural prompts (code, chat, math and long-form; `serving_prompts.json`) greedy at several client counts, optionally behind nonce-led filler documents, and records per-request TTFT and decode rate, aggregate throughput, acceptance, depth selections, per-level memory windows and every output's text; `compare` checks byte-identical output across receipts and levels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): observe in-flight charges without racing event delivery Under a loaded full suite the owner test's event callbacks ran after the request had retired, so it never saw the request charge beside the lane. The test now holds the request before its native terminal, reads the budget there (request charge and lane), then lets it retire. The image cancel case accepts a request that finished before its first chunk was consumed, since the fixture can buffer every token first; it still requires no other error, native retirement and a usable bridge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(libs): pin mlx-swift-lm at the Super decode fusion head 484482d6 The SDK head adds bit-identical fused router-select, expert combine, Mamba step and residual-norm decode kernels; the harness template lists their tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): serve 65,536 tokens of context, accept up to 262,144 The managed slot serves and advertises 65,536 tokens (was 16,384). The bridge builder, the image binding and the runner accept up to NemotronSuperServingPolicy.maximumContextTokens (262,144), verified on the full checkpoint: needle retrieval from 31,935 to 261,160 tokens and finite logits through 131,056 tokens with each prefill chunk inside the lane. The served context is bounded by the default speculating owner: its assistant's fixed request charge grows 30,216 bytes per token of the configured window. At 65,536 it is 4,429,739,396 bytes and eight requests' LOAD claim (36 GiB of KV headroom) is admitted on 256 GB; at 131,072 the claim is refused. The speculative request bound, the admission KV, the discovery footprint (136.3 GiB) and the media charge (sixteen largest images now fit the context) follow; the plain owner's fixed bytes do not change. NemotronSuperLongContextLiveTests (gated) prefills the pinned checkpoint to the served context and checks finite logits and the per-chunk transient against the prefill lane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(nemotron-super): the default claim with the speculating owner's bank At the 65,536-token context the default eight speculating requests with the 1 GiB prefix bank claim 130.7 GB at LOAD and are admitted on 256 GB (measured minimum grant 39,507,064,973 bytes, below the 37 GiB claim). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): pin the bank in the slot floor; runner managed load claim The re-slice floor and the minimum grant of a Super owner include its prefix bank exactly once. A re-slice cannot safely give the bank up today: the engine only lowers the bank's own limit to a quarter of the new grant and keeps entries pinned by in-flight restores and captures, and the shared ledger holds the bank's whole reservation until native retirement, so a floor without the bank would hand a newcomer memory the bank still holds. A test pins the difference at the bank's budget. The qualification runner's --managed-load-claim holds the managed slot's LOAD KV headroom (requests' fixed bytes, 2 GiB and the default bank) instead of the whole grant, so a production-sized grant loads as it would in the provider. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): gated long-context fact retrieval through the slot With `DARKBLOOM_SUPER_NEEDLE=1` the live suite asks for one fact from a synthetic document of about 32,000 tokens through the managed slot's served context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(nemotron-super): bound the default bank by the load's ledger room The builder reserves the prefill lane and then the bank while the LOAD claim (installation, any head and admission KV including the bank) is still held and the installed weights already count as MLX usage. On the 256 GiB machine a 16 GiB default bank therefore did not fit: the production-sized claim was refused on the full checkpoint. The default is now also at most half of the memory cap less the activation reserve, twice the LOAD claim bytes, the requests' fixed bytes and the lane bound. A lone slot gets no default bank on 192 GiB, 8.5 GiB for the speculating owner (16 GiB plain) on 256 GiB and 16 GiB on 512 GiB. The loadPlan and the bank config take the machine's memory for tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(nemotron-super): one LOAD claim holds the pinned owner, each byte once The managed load kept its whole LOAD claim (installation, head and admission KV with the bank) until the owner was built, while the installed weights already counted as MLX usage, and then admitted the prefill lane and the bank on top: the weights and the bank were counted twice. A 16 GiB bank failed to load on 256 GB, and the speculating owner could not pass the post-install recheck on 192 GB with any bank. The pinned checkpoint's single claim, taken before any allocation, now also holds the lane at its measured bound and the bank (NemotronSuperLocalBridgeBuilder.pinnedOwnerShares), with the bank in the claim once. A load that cannot hold its whole owner is refused before a shard is read into memory. Once the installation and head are resident, their share passes to live usage (reducePendingLoad, as the ordinary managed load does), and the builder hands the lane and bank shares to their own reservations right before taking them. The default bank's load bound counts the claim once: a lone slot gets 16 GiB on 192, 256 and 512 GiB. Tests pin one claim that refuses one byte short and loads exactly, and the arithmetic per machine size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): per-token draft history charge; runner MLX limits The bridge prices a sequence-scaled MTP declaration linearly: each request's shared reservation adds the assistant's 30,216 bytes per token of prompt plus max_tokens beside the 8,192 of KV, and the projection's slack joins the fixed bytes so the reservation bounds the engine's charge. The speculating owner's fixed request bytes fall from 4,429,739,396 at 65,536 configured tokens (6.41 GB at 131,072) to 2,450,016,143 at any context; eight speculating requests at 131,072 configured tokens need a 22.8 GB minimum grant and load. The policy's speculative bound becomes 2.5 GiB, the default admission KV 22 GiB beside the bank and the discovery footprint 122.3 GiB. The runner now pins MLX's memory limit and 8 GiB buffer-cache cap with MLXMemoryGuard before loading, as the managed load does; without it MLX kept freed buffers to its default limit and the shared budget, which counts cached buffers as used, refused work the provider admits. Its ready file reports the limits and each request's per-token rates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(nemotron-super): LOAD claim holds the larger sequential transient, not both The managed load claimed the installation quote (payload plus staging and backend allowance) and the head supplement (payload plus 4x construction) as a sum, although the installation releases its staging before the head is constructed. NemotronSuperLoadClaim now claims both payloads and the larger transient, and the load returns MLX's cached staging buffers (synchronize, clearCache) before the head loads, under the same MiMo reclamation guard as the shared budget's reclaimer; when reclamation is unsafe at claim time both transients are summed as before, and if it becomes unsafe after installation the load fails closed. Pinned checkpoint: claim 83,870,320,160 bytes (was 90,948,937,344); the plain claim is unchanged. Measured on the full checkpoint at the previous head (37 GiB grant, eight speculating requests): MLX active peaked at 76,711,620,772 bytes and the process footprint at 78,794,480,240 while the LOAD charge was held. The scan-time footprint drops from 136.32 to 129.73 GiB. The runner logs the claim's parts in load.quote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): gated full-checkpoint memory ledger NemotronSuperMemoryLiveTests (DARKBLOOM_SUPER_MEMORY_LIVE=1 and NEMOTRON_SUPER_CHECKPOINT_DIR, exclusive GPU) measures MLX active memory, its peak and the process footprint around each native phase: installation and head against their quotes, every 2,048-token chunk of one prompt and decode steps on contiguous and paged storage (bytes per context token and chunk transients against the prefill lane), one chunk under the stage profile's memory recording, and the largest image grid's tower against the media activation bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): runner emulates a smaller Mac's memory --emulate-physical-gib N builds the runner's process budget with an N GiB total and this machine's available memory minus what N GiB lacks, sets MLXMemoryGuard's limits for N GiB before any weights, and applies the managed slot's load gate (scan-time footprint plus activation reserve and minimum KV against available-for-load memory) before loading, refusing as the provider would. Qualification only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(nemotron-super): name every managed load refusal A refused managed Super load recorded "native construction was refused" for any failure outside a short list, which hid the post-install LOAD claim recheck (loadAdmissionLost), a refused decode-row reservation and a bank the engine did not construct. Each now has its own reason; only an error from outside the load keeps the generic one. The reasons carry no paths or payload data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(nemotron-super): plan the prefix bank the builder builds The load plan sized the bank as the pinned checkpoint's from the host's memory, while the builder sized it from the installed checkpoint, so an owner that is not the pinned checkpoint planned an admission for one bank and built another, and the result depended on the machine running it. The slot loader now carries both inputs, its pinned-ness and the machine memory it sizes for; the plan, the LOAD claim's shares and the builder all read them. The production loader passes the pinned checkpoint and the host's memory, so its behavior is unchanged. The managed-slot fixture takes its own machine memory. Tests pin plan bank == built bank for pinned and generic owners at 64 and 256 GiB, the fixture holding exactly its planned bank on both, and derive the policy expectations from admissionKVBytes instead of literals that predate the 65,536-token context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prefix-cache): how the recurrent bank's default is sized The hybrid bank takes DARKBLOOM_CBV2_HYBRID_PREFIX_BYTES, else a model policy's own default (the pinned Nemotron Super owner's, from the grant and the machine), else min(1 GiB, grant / 8). The configuration reference counted the Super LOAD claim twice in the bank's load bound; the claim now counts it once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): emulation as a dedicated Mac, with its grant and bank The emulated budget's available memory is the emulated total minus this process's footprint (other processes are assumed to fit beside the cap), never above what this machine really has; the default prefix bank is sized for the emulated Mac (an explicit size, or none when it does not fit), and load.quote logs the KV grant the provider's re-slice would give there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(nemotron-super): read the lane and LOAD bytes from one policy function each The LOAD claim's lane share, the builder's lane, the default bank and the discovery footprint now read the pinned lane bound from NemotronSuperLocalPrefill.pinnedLaneBytes, the requested chunk from requestedChunkTokens, and the pinned LOAD bytes from NemotronSuperServingPolicy.pinnedLoadClaimBytes, so a new lane bound, installation claim or per-request charge reaches every one of them. No value changes; a chunk key above 65,536 tokens (up to the 262,144 maximum) now sizes the default bank as it sizes the lane share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(libs): pin the Super decode command-buffer heads mlx-swift-lm e03757bc scopes larger Metal command buffers to the Super owner's plain decode steps and batches the full SSD prefill windows (still bit-identical); mlx-swift 45c7461 and mlx 5e28974c add the thread-scoped command-buffer commit limits it uses (C++ only, no kernel change). MTP rounds are not scoped. The harness template lists the new MLXVLM source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(nemotron-super): the load transient runs in the serving room it precedes The single LOAD claim reserves the owner's lane, bank and KV floor beside the installation and head, but none of that room is allocated before the load has returned its transient buffers. With reclamation the claim now adds only the transient beyond that room: both payloads plus max(transient, serving room) in all. The plain owner also returns the installation's cached buffers before serving, and either phase fails closed if reclamation became unsafe. Without reclamation both transients are claimed beside the room, as before. The scan-time footprint is the resident payloads plus the larger of the transient and the serving room (lane, admission KV, one image): 121.45 GiB for the default configuration (was 129.73 after the sequential-transient fix, 136.32 before it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): live ledger reports KV rows and a fenced-prefill arm The gated ledger prints every attention row's storage type, dtype and bytes after the first and last chunk and after decode, and repeats the first three chunks with the residual stream evaluated every 8 and every 2 layers (setSuperPrefillFenceLayers, measurement only), timed, to separate the chunk's own working set from intermediates retained by queued command buffers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(nemotron-super): measured 2 GiB activation floor for the Super slot The 5.5 GiB default reserve is sized for models whose request charges hold only KV. Super's requests also reserve their decode row, recurrent generations and verify/draft workspace, its owner-wide lane covers prefill chunks and its media charge each image's tower and projector, so the reserve counted those activations twice. Full checkpoint, M3 Ultra, eight ~500-token requests with default speculation: MLX peaked 5.5 GB above the pre-request resident set while 27.6 GB of request charges were held; the only memory no charge covers is what the owner keeps resident after its requests (0.94 GB speculating, 0.23 GB plain). Floor: 2 GiB. A serving set with an unmeasured model keeps the default; the operator's raise-only override still applies. The platform's servability table must carry the same entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(nemotron-super): the LOAD claim's transient, the 2 GiB floor and the memory tools hardware-support and inference describe the claim (resident payloads, the serving room, the larger load transient only beyond that room) with the measured load peak, the 121.5 GiB footprint and the 124.5 GiB gate under the 2 GiB floor; the constants table lists the Super floor. The runner doc covers --emulate-physical-gib, the load.quote parts and the gated memory ledger test; the changelog fragment carries the footprint and the reserve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): reserve the speculating owner's round work once The SDK now reports the assistant's per-owner MTP bytes (`ownerBytes`): the depth policy's largest round of draft steps, one history chunk, scratch, the verify window, the captured windows' extra recurrent generations and the worst-case whole-history copies. Requests no longer carry these. The local bridge builder reserves them in the shared budget after the decode rows and before the bank, holds them until native retirement like the lane, and refuses the build (`roundRefused`) when the budget cannot hold them. An unresolved declaration reserves Int.max, so it always refuses. The runner reports the reservation as the `mtp_round` charge row. Its ready file adds `request_charge`, which breaks one request's fixed and per-token charge into engine and bridge fixed bytes, recurrent generation, decode row, the projected MTP split with its owner, window and step counts, and the fixed, owner and per-token declarations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): the Super plain decode command buffers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): the one-claim boundary follows the serving-room overlap oneLoadClaimHoldsThePinnedOwnerOnce computes the claim as the transaction does: the installation's payload and only the staging beyond the lane, bank and KV floor it also holds. One byte less free is still refused before anything is installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): the runner holds the lone Super slot's activation reserve The runner's budget now carries the serving-set reserve the provider loop sets for a Super-only serving set (its measured 2 GiB floor) instead of the 5.5 GiB process default, so its ledger and the emulated Mac admit as the managed slot does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(nemotron-super): split the speculating admission into request and owner bounds The LOAD claim's KV headroom charged 2.5 GiB per speculating request, which was measured while each request carried a whole round of MTP work. The SDK now reserves that work once per owner (`mtp_round`), so the headroom is split the same way: - Per request: 576 MiB (measured 533,401,365 at depth 3; 512 MiB plain is unchanged). - Once per owner: `speculativeOwnerBoundBytes`, computed from the engine's own counts for the depth table in effect. These come from CBv2MTPAllocationLimits: round draft steps, window positions, captured-window generations and worst-case history copies. Each count is multiplied by its pinned unit (from the ready charges). The total gets 64 MiB of allocator slack and is rounded up to 64 MiB. - Default table, 8 requests, 65,536 tokens: 5.03 GB, against a measured 4,935,449,637. - One request at 133,120 tokens: 2.89 GB, against a measured 2,791,734,886. - It shrinks to 4.36 GB with "3,2,1". A malformed setting counts as adaptive at the largest depth (11.9 GB). - Eight speculating requests now claim 11.19 GiB of headroom instead of 22 GiB. The discovery footprint drops from 122.3 to 111.5 GiB. The round is a third LOAD-claim owner share beside the lane and the bank. The builder hands each share over right before taking its exact reservation, in the order lane, round, bank, so no byte is counted twice. It still refuses the build when the engine's exact `ownerBytes` does not fit. NemotronSuperLocalMTP.config builds the CBv2MTPConfig for both the engine and the policy. The runner passes its environment and `--max-context` to the managed admission and the default grant. Its `request_charge` reports `policy_owner_bound` and `history_copies` beside the engine's exact `owner`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(nemotron-super): the 8.5 GiB measured prefill lane hardware-support carries the lane at the SDK's measured bound and the footprint and gate it gives (118.0 and 121.0 GiB; 134.4 GiB of physical memory, 192 GB leaving 51.8 GiB). inference separates the first chunk's whole-prompt KV allocation (charged to the request) from a chunk's own transient (7.50-7.67 GB measured, inside the lane). The changelog fragment follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(nemotron-super): default bank sizes under the split admission The default bank is half of what the grant leaves beside the admission, the lane and one image. Eight speculating requests now claim 11.19 GiB instead of 36 GiB, so the bank at the sample grants is 3.5, 9, 15, 16 and 16 GiB (34, 45, 60, 100 and 155 GiB grants). The chunk comparison moves to a 50 GiB grant, below the 16 GiB cap, where the 12 GiB and 20 GiB lanes leave 11.5 and 7.5 GiB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(libs): pin mlx-swift-lm at the Super long-context head 82fe57b1 The SDK sizes speculation state once per owner and per request and charges the assistant's growing state per reserved token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(libs): pin mlx-swift-lm at the Super memory merge 620766e9 The SDK prefill lane bound is the measured chunk peak, and the installation load quote is split into payload and staging. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin corrected Nemotron Super workspace documentation * Preserve Super distribution notices and qualify managed effort controls * test(nemotron-super): encode effort acceptance bodies before the live client Swift 6 rejected the @sendable live client closure capturing [String: Any] controls and a local body builder. Every request body is now encoded to Data before the closure; the cases and assertions are unchanged. The full-checkpoint run passed: 16 control variants, 6 tool and 6 JSON cases, 4 refusals and exact cold/warm results for high, medium and none. * feat(nemotron-super): serve paged KV with exact prefix reuse by default The managed Super slot could use paged attention KV or the exact recurrent prefix bank, never both: the builder refused the combination and the plan kept the bank off on paged storage. SDK 53b11b32 lets the bank host on segmented paged KV (`CBv2HybridPrefixCacheConfig.permitsPagedStorage`). - `NemotronSuperServingPolicy.loadPlan` selects paged KV unless the operator sets `DARKBLOOM_NEMOTRON_SUPER_KV_BACKEND`, and sizes the default bank the same way on either storage. The builder alone still defaults to contiguous. - The Super bank config opts into paged storage; the factory carves the bank from the paged grant as from the contiguous one and hands it to the engine. A paged backend that cannot host it fails the load (`unavailable`). - The `pagedPrefixReuse` refusal and the paged bank exclusion are removed. Synthetic owners on paged storage restore the deepest chunk boundary after another request reused the donor's pages and stream the cold paged text, plain and speculating; managed-slot fixtures now run with 64-wide attention heads, the paged kernels' minimum, so they exercise the default. A paged slot with a bank still claims its whole grant (`slotKVBytesClaim`): its segmented pool follows the grant minus the bank carve, as contiguous storage does, so co-resident heartbeats do not count the bank as free. * docs(nemotron-super): gate the model to Apple M5 Max 128 GB and M5 Ultra The platform registration now lists min_ram_gb 128 and required_provider_capabilities ["apple_m5"], so Nemotron 3.5 Super is offered only to attested M5 providers with at least 128 GB (M5 Max 128 GB, M5 Ultra). Replace the stale 192 GB floor in the hardware requirements, hardware support, registry reference and changelog fragment with that gate and the existing load-gate arithmetic: the default eight-request speculating owner fits a 128 GB cap with 19.1 GiB to spare, but not beside a 16-image request. Only the 256 GB M3 Ultra is measured on hardware; a physical M5 Max 128 GB run is still pending. * fix(nemotron-super): honor or refuse every reasoning control, never ignore one Live checks showed chat_template_kwargs.reasoning_effort, chat_template_kwargs.thinking, reasoning.max_tokens, reasoning.exclude and reasoning.summary accepted with 200 and ignored. NemotronSuperPromptContract.controls now honors OpenRouter's chat_template_kwargs.thinking as enable_thinking (same conflict rules as every spelling) and accepts Responses reasoning.summary levels auto/concise/detailed (the full reasoning is returned). It refuses (unsupportedControl, HTTP 400) any other chat_template_kwargs key and any other reasoning member unless null or exclude:false. Effort levels none/off/0, medium and high are unchanged. The coordinator's prompt sidecar applies the identical rule (Layr-Labs/darkbloom-platform#118). * feat(nemotron-super): serve the full verified 262,144-token context The managed slot capped its served and advertised context at 65,536 for prefill time and long-document recall. Those are routing decisions for the coordinator, not a provider cap: contextTokens is now maximumContextTokens (262,144), the ceiling verified on the full checkpoint. Only the speculating owner's whole-history copies grow with the window (16 copies for the default table and eight requests, 1,536 bytes per token each): its round bound goes from 5,033,164,800 to 9,865,003,008 bytes (the engine reserved 9,767,287,845 on the full checkpoint), admission KV from 11.2 to 15.7 GiB and the default discovery footprint from 93.14 to 97.64 GiB. On a 128 GiB Mac the load gate needs 100.6 GiB under the 115.2 GiB cap and the default prefix bank is 5.25 GiB. Tests keep the measured references pinned at 65,536 and assert the served value, the round bound and the footprint at 262,144; the 128 GiB-Mac claim test runs at both. Docs, the runner guide and the changelog fragment carry the new figures. * Pin mlx and mlx-swift to the merged command-buffer limits libs/mlx moves from the PR head 5e28974c to 71271f08 (squash-merged Layr-Labs/mlx#36, which also exports the entry point with MLX_API) and libs/mlx-swift from 45c74613 to 81f58d79 (squash-merged Layr-Labs/mlx-swift#57, which pins that mlx and adds the API's tests). Same API and Metal kernels. --------- Co-authored-by: Jonathan Spangler <5055076+jonathan308@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Gajesh Naik <26431906+Gajesh2007@users.noreply.github.com>
23 of 27 tasks
jonathan308
added a commit
that referenced
this pull request
Oct 22, 2026
Brings the cluster stack onto the single-Mac release chain. Every single-Mac commit except three is a cherry-pick of a commit this branch already has, so the conflicts (unified_memory.rst, custom_kernel.cpp, process_allocation_cap_metal.cpp, fork.yaml) keep this branch's side, which also carries the later fenced-input, host-cut and sealed-sum changes. New here: - #36 thread-scoped command-buffer commit limits (merged cleanly; its fork.yaml section is added by hand). - The single-Mac revert of the page-backed footprint commit does not travel through this merge (its net change on the single-Mac side is zero); the next commit reverts this branch's original 4d91320 explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 of 10 tasks
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.
Summary
Adds a per-thread override for when Metal command buffers commit.
mlx_metal_set_thread_command_buffer_limits(max_ops, max_mb, &prev_ops, &prev_mb)makes command buffers encoded on the calling thread commit aftermax_opsoperations ormax_mbmegabytes of distinct inputs, instead of the device defaults. Zero keeps a default, and the previous values are returned so the caller can restore them.Evaluation encodes on the thread that calls
eval/async_eval. A caller can therefore give single-token decode steps larger buffers, so the CPU encodes further ahead of the GPU, while prefill keeps the defaults and the memory bound they give. On Ultra parts the defaults are 50 ops / 50 MB, and the MB counter includes weight inputs, so multi-GB MoE expert banks force a commit after almost every expert operation.Partitioning work into command buffers does not change results. The device defaults and the
MLX_MAX_OPS_PER_BUFFER/MLX_MAX_MB_PER_BUFFERenvironment overrides behave as before when no thread limit is set.fork.yamldescribes the change, andscripts/check_forkdiff.pypasses against upstreammain.Why not the environment variables
MLX_MAX_OPS_PER_BUFFER=1000 MLX_MAX_MB_PER_BUFFER=4000speeds up decode just as much. But the setting is process-wide: temporaries live until their command buffer completes, so an 8K-token prefill of Nemotron 3.5 Super peaked about 75 GB higher.Layers
GPU.withCommandBufferLimits)Test
Full Nemotron 3.5 Super NVFP4 checkpoint, M3 Ultra (60-core GPU, 256 GB), release build. Greedy decoding, 27-token prompt, 64 tokens. The engine scopes only single-token plain decode steps, at 100 ops with no MB limit. Prefill, mixed steps and speculative rounds keep the defaults.
Merge Danger
Door: two-way. This is an additive C entry point and a thread-local check in
Device::get_max_ops_mb_per_buffer. With no thread limit set, behaviour is unchanged.🤖 Generated with Claude Code