MAX nightly
This version is still a work in progress.
Documentation
- Added a dedicated metrics reference page with all available Prometheus metrics, categorized by subsystem. The metrics section in the container page now links to the new page.
- Added an audio generation guide, covering serving
a text-to-music model over
/v1/audio/speechand/v1/responses, the request fields and their defaults, the lyric tag syntax, and the length a single render is capped at. - Added a music generation example that renders songs past that per-render cap by rendering sections and joining them, and checks the joins for audible seams.
MAX models
- Added the
audio_generationpipeline task, for models that render audio rather than tokens or pixels. Its request options (lyrics, duration, denoising steps, guidance scale, output format) arrive as theaudioprovider options of an OpenResponses request, and an architecture on the task serves over/v1/audio/speechand/v1/responses. Responses reportusagethe way image generation does: token counts stay at 0 and ausage.audio_generation_detailsblock carriesduration_seconds,sample_rate,channels,num_samples, andsteps, measured from the audio actually produced rather than the duration that was asked for. - Added MiniMax-Music3 (
MiniMaxMusic3ModularPipeline) support, the first architecture on theaudio_generationtask: a text-to-music model that renders a style caption plus lyrics into 44.1 kHz stereo audio. The five component networks exceed a 24 GB card together, so the pipeline builds and releases each stage in turn within a request; the first request after a cold start pays a multi-minute compile that later ones replay from the compilation cache. - Startup no longer prints one
unknown dtype found in safetensors filewarning for each tensor with a dtype that is not a weight encoding. Each scan of the weight files now prints one warning for each unknown dtype. - Fixed DeepSeek V3 losing tool-call parsing when served with Eagle3 or MTP
speculative decoding. The fused spec-decode architectures now inherit the
base architecture's settings instead of redeclaring them, so
--tool-parserno longer has to be passed by hand. - Fixed unbounded host-memory usage in Gemma 4 video pre-processing: the server now decodes only the sampled frames of a video instead of materializing every frame before sampling, bounding peak memory at the sampled frame count (previously a long clip could transiently allocate ~100 GB in the API server process).
- Added GLM-5.2 (
GlmMoeDsaForCausalLM) support, extending the GLM-5.1 sparse-attention architecture with cross-layer index sharing.- Added multi-token prediction (MTP) speculative decoding for GLM-5.2
(
UnifiedMTPGlm5_2ForCausalLM), serving the baked-in NextN layer as a single-layer sparse-MLA draft; enabled automatically for GLM checkpoints that ship a NextN layer with--speculative-method mtp. - Added tool-calling, reasoning, and structured-output (
response_format) support to GLM-5.1 / GLM-5.2, enabled with--tool-parser glm45 --reasoning-parser glm45 --enable-structured-output. - Fixed a GLM-5.1-FP8 crash caused by a shared-experts dtype mismatch.
- The GLM-5.2 B200 recipe now serves the checkpoint's full 1M-token
context window (
max_length: 1048576, previously pinned to 163840). The pin existed because the wider window cost ~33% decode throughput on long-context workloads; the sparse-attention indexer now does work proportional to actual sequence lengths (per-layer kernel cost measured flat across frozen bounds), and a weekly long-context serving benchmark tracks the end-to-end throughput at this configuration.
- Added multi-token prediction (MTP) speculative decoding for GLM-5.2
(
- Added multi-token prediction (MTP) speculative decoding for Inkling
(
UnifiedMTPInklingForConditionalGeneration), serving the checkpoint's chained dense draft depths; enabled automatically for Inkling checkpoints that shipmtp_configwith--speculative-method mtp. - Added Laguna (
LagunaForCausalLM) support forpoolside/Laguna-M.1-NVFP4, including tool calling. - Added DiffusionGemma (
DiffusionGemmaForBlockDiffusion) support forgoogle/diffusiongemma-26B-A4B-it(bfloat16) andnvidia/diffusiongemma-26B-A4B-it-NVFP4; text-only for now. - Added Nemotron-H (
NemotronHForCausalLM) support, NVIDIA's hybrid Mamba-2 + attention decoder, with modelopt per-tensor FP8 and a new Mamba-2 SSD chunked-scan varlen kernel.- Extended Nemotron-H with the Nemotron-3-Nano-30B-A3B hybrid MoE variant and enabled the architecture on Apple silicon GPUs in bfloat16.
- Enabled NVIDIA's official FP8 Nemotron-H checkpoints on Apple silicon (previously crashing or producing all-zero logits) and sped up Nemotron-H decode on Apple M5 by ~41-81%.
- Added support for serving
nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8on Apple silicon via a tiled simdgroup-MMA grouped-FP8 (W8A16) MoE matmul, decoding faster than bf16 at concurrency with half the weight memory.
- Fixed the
max_batch_sizehandling for Nemotron-H. - Added support for the
detailparameter on image and video content parts in chat requests. - Added Ideogram 4 (
Ideogram4Pipeline) support, a text-to-image flow-matching diffusion transformer; serve via/v1/responses.- FP8 checkpoint weights run hot projections on native FP8 GEMMs (~24% faster end-to-end on MI355).
- Added support for
amd/Kimi-K2.7-Code-MXFP4on AMD GPUs. - Expanded Gemma 4 support:
- Added DSpark speculative decoding for Gemma 4 12B
(
UnifiedDSparkGemma4ForCausalLM), DeepSeek's block-drafting method: a small draft transformer drafts a 7-token block per step. Enabled with--draft-model-path deepseek-ai/dspark_gemma4_12b_block7 --speculative-method dflash --num-speculative-tokens 7.- Fixed the draft applying full rope instead of the checkpoint's partial rotary factor (0.25), which was costing roughly 10% of the draft acceptance rate.
- Added DSpark speculative decoding for Gemma 4 31B
(
UnifiedDSparkGemma4_31BForCausalLM), servinggoogle/gemma-4-31B-itwith the vLLM speculators-format draftRedHatAI/gemma-4-31B-it-speculator.dspark(llama-style causal draft block, pruned 32k draft vocabulary mapped through the checkpoint's d2t table). Enabled with thegemma4_31b_dspark.yamlrecipe or--draft-model-path RedHatAI/gemma-4-31B-it-speculator.dspark --speculative-method dflash. An explicit--num-speculative-tokensis honored: values below the trained 7 truncate the causal draft block prefix-stably, values above run as extrapolation with a warning and degrading acceptance; unset defaults to the trained 7. - Added DFlash speculative decoding for Gemma 4 31B
(
UnifiedDflashGemma4_31BForCausalLM), servinggoogle/gemma-4-31B-itwith the z-lab block-diffusion drafterz-lab/gemma-4-31B-it-DFlash: a 5-layer noncausal draft block drafts 15 tokens per step from six target hidden-state taps. Enabled with thegemma4_31b_dflash.yamlrecipe or--draft-model-path z-lab/gemma-4-31B-it-DFlash --speculative-method dflash. The draft width is pinned to the drafter's trainedblock_size - 1; a mismatching--num-speculative-tokensis overridden with a warning. NVFP4 target checkpoints (nvidia/Gemma-4-31B-IT-NVFP4) are supported via thegemma4_31b_dflash_nvfp4.yamlrecipe. - Gemma 4 31B DSpark now supports structured output (JSON schemas and
tool-call grammars, enforced on the target verify pass; a
grammar-violating draft is rejected at its position) and Gemma 4
thinking: reasoning content is split out of responses, and relaxed
acceptance during the thinking phase can be enabled with
use_relaxed_acceptance_for_thinking. - Renamed the Gemma 4 12B DSpark architecture to
UnifiedDSparkGemma4_12BForCausalLM(modulemax.pipelines.architectures.unified_dspark_gemma4_12b), so the two Gemma 4 DSpark architectures are named by model line. - Sped up Gemma4-12B DSpark decode by up to ~1.3x via a packed wide-N shallow-K GEMV, a single-pass streaming argmax kernel, and device graph capture.
- Gemma 4 with MTP speculative decoding (
UnifiedMTPGemma4ForCausalLM) now supports image and video input; previously the vision encoder output never reached the language model, so image prompts were answered as if the model were blind. - MTP speculative decoding now samples recovered tokens from the residual distribution when stochastic acceptance rejects a draft token, preserving the target distribution for argmax draft proposals.
- Added structured-output and tool-calling support via the xgrammar backend, covering Gemma 4's special tool-call format.
- Added float16 support, with the logit softcap and vision pooler run in fp32.
- Added tensor-parallel support for the MoE variant.
- Video inputs now route through the shared
VisionEncoderCache, so a repeated clip is served from cache with no re-encode. - Video decoding now runs on a worker thread, so concurrent requests overlap video decode.
- Improved vision-batch serving latency by concatenating embeddings on-device instead of round-tripping through host numpy.
- Fixed the MoE expert-router softmax being computed in
bfloat16instead offloat32, which degraded MoE quality. - Fixed image/video position and scatter indexing desyncs under chunked prefill, which could corrupt vision embeddings on multimodal prompts split across chunks.
- Fixed crashes in multi-device serving and multi-image batches by making
merge_per_device_buffersrank-agnostic. - Fixed reasoning being dropped after tool results.
- Fixed a vision-batch crash caused by constructing a
Device()instead ofCPU()for host tensors.
- Added DSpark speculative decoding for Gemma 4 12B
(
- Expanded DeepSeek-V3 ModuleV3 support:
- Added NVFP4 (modelopt) weight support, running experts, dense MLPs, and the attention output projection on SM100 block-scaled FP4 matmul kernels.
- Added data-parallel + expert-parallel (DP-EP) and multi-GPU
tensor-parallel + expert-parallel (TP+EP) serving. Note:
Tensor.tono longer implicitly callsF.distributed_broadcast; call it explicitly where needed. - Fixed the FP8 adapter by casting f32 normalization gammas, resolving a dtype mismatch.
- Expanded Kimi K2.5 support:
- Kimi with DFlash speculative decoding
(
UnifiedDflashKimiK25ForCausalLM) now supports image input; previously the vision encoder was not compiled, so image prompts were answered as if the model were blind. - Added support for combining Kimi tool calling with
response_format=json_schemaon the xgrammar constrained-decoding backend.
- Kimi with DFlash speculative decoding
(
- Expanded FLUX.2 support:
- FLUX.2-klein bf16 checkpoints on Apple M5 GPUs now default to int8
W8A8 quantization, ~1.45x faster end-to-end than bf16 on
FLUX.2-klein-4B at near-lossless quality; set
APPLE_FLUX2_INT8_W8A8=0to opt out. - NVFP4 checkpoints can now opt into an int8 W8A8 requant at load on
Apple M5 with
APPLE_FLUX2_INT8_W8A8=1, ~2.56x faster end-to-end than the default W4A16 path on FLUX.2-dev. - Diffusion pipelines now support two denoising-cache backends to skip
redundant transformer passes:
--taylorseer(recommended default, withbalancedandfastpresets) and--first-block-caching; the two are mutually exclusive and both off by default.
- FLUX.2-klein bf16 checkpoints on Apple M5 GPUs now default to int8
W8A8 quantization, ~1.45x faster end-to-end than bf16 on
FLUX.2-klein-4B at near-lossless quality; set
- Expanded Qwen support:
- Added tool-calling and reasoning support to Qwen 3.5 / 3.6.
- Added
Qwen/Qwen3.8-27Bsupport in bfloat16 on the existingQwen3_5ForConditionalGenerationarchitecture, covered by logit verification against the torch reference. Qwen3_5ForConditionalGenerationnow serves across multiple GPUs. Tensor parallelism splits the attention heads, the gated-DeltaNet key and value heads, and the per-device linear-attention state pools; both mixers reject a device count that would not divide their head counts evenly.Qwen3_5ForConditionalGenerationnow supports device graph capture.- Added multi-token prediction (MTP) speculative decoding for Qwen3.8
(
UnifiedMTPQwen3_5ForConditionalGeneration), fusing the target, the baked-in MTP head and a recurrent-state rollback into one graph, selected for Qwen3.5-family checkpoints that ship an MTP head with--speculative-method mtp. Rejecting a speculated token cannot be undone by rewinding a KV length pointer when the layer is recurrent, so the graph snapshots the gated-DeltaNet conv and recurrent pools before verifying and replays the two state kernels over the accepted rows. The graph is served through the Mach engine; MAX compiles and exports it but does not run it. - Fixed a
Qwen3EmbeddingModelcrash.
- Added
--state-pool-dtype, which overrides the storage dtype of a hybrid model's recurrent state pools (SSM and linear-attention conv and recurrent state). It defaults to the model's compute dtype.float32makes a speculated generation follow the same state trajectory as an unspeculated one -- the recurrence rounds to the pool dtype at each call boundary, so a lossy pool makes the trajectory depend on how speculation chunked the sequence -- at roughly double the per-request state memory (Qwen3.8-27B: 74.8 to 149.6 MiB per seated request). - Added per-request LoRA adapter support:
LoRALinearandStackedLinearLoRAextend LoRA to standalone and fused-QKV projections, withLoRAManager.applyswapping target layers in a model. - Improved Eagle3 speculative-decoding performance by removing a redundant concatenate in the draft path.
- Fixed Step-3.5-Flash accuracy and performance.
- Fixed the EAGLE3 MHA draft
lm_headall-gather in pure tensor-parallel mode.
MAX framework
-
Added
Device.is_host_unified(max.driver) andDeviceContext.is_host_unified()(Mojo): whether a device and the host draw from one physical memory pool. Reports hardware topology, so it does not imply any given buffer is host-readable. Driver plugins answer it through the new optionalhost_unifieddevice property. -
Host-side profiling spans (
max.profiler.Tracer,@traced, and MojoTracescopes) now annotate external profiler tools on release builds: withMODULAR_ENABLE_PROFILINGset, spans appear as NVTX ranges in NVIDIA Nsight Systems captures and as roctx ranges in rocprofv3 captures, with no build flags required. Previously these spans were only emitted in special profiling builds. -
Greedy speculative acceptance (
greedy_acceptance_sampler,AcceptanceSamplerin greedy mode) now applies the structured-output grammar bitmask to the target logits (with a-inffill) before the argmax, so a grammar-invalid draft is always rejected and recovered and bonus tokens always satisfy the constraint — matching the stochastic path. Unconstrained batches are unchanged. -
stochastic_acceptance_samplerandAcceptanceSampleralso accept a rank-1[batch_size]per-row seed tensor in stochastic argmax mode: each row's acceptance sampling is then keyed off its own seed instead of row 0's, so a row samples independently of its co-residents. A single-row batch is bit-identical to the scalar-seed behavior. The gemma4 and qwen3.5 unified MTP graphs now pass their per-row seed tensors through. -
Fixed
response_formatschema normalization skipping containers the grammar backends compile: an untyped object-shaped subschema underadditionalProperties,unevaluatedProperties,unevaluatedItemsordependentSchemasis now anchored to an object, as one underpropertiesalready was. Such a subschema previously compiled to a grammar admitting an unbounded value, letting a looping model run tomax_length. -
Added the experimental
--experimental-device-graph-synthesisflag (PipelineRuntimeConfig.experimental_device_graph_synthesis): compiles model graphs with device-graph synthesis, so the compiled model records its kernels into a device graph and replays it on execute. Honored only by architectures that opt in (currently Gemma 4's language graph), and mutually exclusive withdevice_graph_capture. -
Added
max.pipelines.lib.MemoryPlan, the result of memory planning when a pipeline is loaded: the effectiveplanned_max_length,max_batch_size,max_batch_total_tokens, KV-cache budget, and device specs the pipeline and its schedulers consume. -
Renamed
MemoryEstimator.estimate_memory_footprinttoMemoryEstimator.plan_from_sizes, after theMemoryPlanit returns. UseMemoryEstimator.planinstead to plan from aPipelineConfigalone;plan_from_sizesis for callers that have already computed the weight, activation, and signal-buffer sizes. -
The sequence-length rule now runs once, when the config is built:
config.model.max_lengthholds the resolved length andPipelineArgs.max_lengthkeeps what the user asked for.ArchConfig.initializereceives that length instead of deriving it (max_seq_lenis now a required keyword argument), and memory planning may only lower it, on the plan.PipelineModel.calculate_max_seq_len,ArchConfigWithAttentionKVCache.user_provided_max_lengthandmodel_max_seq_lenare removed; architectures own the rule, so Mistral, Mistral3 and Pixtral now boundmax_lengthon their configs. -
Memory planning no longer writes its planned
max_lengthandmax_batch_total_tokensback onto the pipeline config. After startup,PipelineConfig.model.max_lengthkeeps the construction-resolved value andPipelineConfig.runtime.max_batch_total_tokenskeeps the user-provided value (Nonewhen unset); the effective values live onMemoryPlan. -
PipelineModelnow requires thememory_planconstructor argument (keyword-only; constructing a pipeline model without a plan raises aTypeError), andPipelineModel.max_seq_lenis a read-only view of the plan'splanned_max_lengthrather than a stored copy with a config fallback. -
Made
MemoryEstimator.free_memory,static_memory_size,available_kv_cache_memory, andmax_supported_sequence_lengthprivate. They are steps within a memory plan rather than useful on their own, and the values they produced are now available onMemoryPlan. -
The block-based vision encoder cache now shards its storage across devices instead of replicating every entry on each one. The same
--vision-cache-utilizationfraction buys the same cache capacity while reserving only1/n_devicesof it per device; the remainder stays with the KV cache. Cache hits gather rows to each device in one batched submission. -
Added opt-in token-balanced CE scheduling across data-parallel replicas. With
--dp-ce-balance-timeout-ms>= 0 (default -1 = off), new context encoding requests wait in an unbound pool and are placed by a per-step planner that prices them at their post-prefix-cache length (a read-only probe of each replica's device cache and the shared host/disk tiers) and binds them to the least-loaded replica when first scheduled. Unbalanced CE work may be deferred up to the timeout while its replica runs decode instead, until per-step occupancy reaches--dp-ce-balance-threshold(default 0.8). A below-threshold step with CE work on two or more replicas still runs immediately with each replica's chunk size reduced to the balance level, so only the excess defers (--dp-ce-balance-enable-dynamic-chunk-size, default on; skipped when the balance level is under half the CE chunk target, where the extra chunks would cost more than the imbalance). -
Added
--chunked-prefill-min-chunk-size(config keyruntime.chunked_prefill_min_chunk_size, default 0 = off) to set a floor, in tokens, on any chunk created by chunked prefill. When splitting a request against the CE token budget, the cut is moved earlier so that neither the chunk nor its remainder is smaller than the floor; if no legal cut point exists within the remaining budget, the request is left unsplit for a later step. This avoids degenerate slivers (for example an 8-token tail chunk after an 8192-token budget cut) that pay a full step's overhead and re-read the request's entire context in attention for almost no progress. -
Fixed non-streaming chat completions leaking a literal structural tool-call marker (for example
<tool_call>) intomessage.contentwhen amax_tokenstruncation landed mid tool-call block. The response now surfaces only the content before the marker, withfinish_reason == "length". -
Added an experimental
--fold-sampler-into-graphoption (default off) that folds greedy token selection (argmax) into the captured forward graph, so a single device-graph replay materializes the sampled token instead of a separate sampler submission with a blocking readback. Applies to all-greedy decode batches on architectures that emit the folded token output (currently Nemotron-H); non-greedy requests fall back to the separate sampler. -
Added a
max-pending-futuresconfig (default 1, the classic overlap-scheduler depth of one forward in flight per request). Request bookkeeping now tracks unrealized future-token placeholders with a counted model instead of a single-sentinel check, and setting the value to 2 enables experimental schedule-ahead decoding: two forwards in flight per request, with the next step's input token realized on-device from the folded sampler output. Behavior at the default depth is unchanged. -
Fixed the serve CLI dropping the
fold-sampler-into-graph,max-pending-futures, and greedy-sampling gate settings on their way to the model worker, which silently disabled the folded greedy sampler. With the flags threaded through,--fold-sampler-into-graphremoves the per-token blocking sampler submission and substantially improves decode latency on architectures that support it. -
Added
max.engine.readfor loading a compiled-model artifact (a.meffile) without anInferenceSession. The resultingCompiledModelcan be initialized on any session viaInferenceSession.init. It replacesInferenceSession.read, which has been removed. -
Image generation responses on the Open Responses endpoint now report
usage: token counts stay at 0 and a newusage.image_generation_detailsblock carrieswidth,height,megapixels,steps, andimage_count, measured from the actual generated images rather than the requested dimensions. Previouslyusagewas alwaysnull. (An interim nightly reported the raw pixel count asoutput_tokens; that encoding is replaced byimage_generation_details.) -
Added
InferenceSession.readfor loading a compiled-model artifact (a.meffile) previously saved withCompiledModel.export_mef. It accepts a path or a binary file-like object (such asio.BytesIO), deserializes without invoking the graph compiler, and returns aCompiledModelready to pass toInferenceSession.init. -
Added
--no-enable-tool-call-constrained-decode(config keysampling.enable_tool_call_constrained_decode, default enabled) to decouple tool-call parsing from constrained decoding. When disabled, a configured--tool-parserstill parses tool calls out of the generated text, but no server-generated grammar is produced and the bitmask constrained-decode path is skipped for tool calls. Note that with it disabled,tool_choice=requiredor a named function can no longer force a tool call. This is independent of--enable-structured-output, which continues to gate user-suppliedresponse_formatJSON schemas. -
Fixed the
codelabel on themaxserve_request_countmetric so it reports the HTTP status code actually returned to the client. The count is now recorded from the HTTP layer, so failures rejected before generation (for example a request with an unreachable image URL) are counted with their real status code instead of being labeled200or dropped entirely. Liveness and observability endpoints (/health,/version,/ping,/metrics) are not counted. -
Failed request submissions in the OpenAI-compatible serving endpoints now surface as HTTP error responses instead of a
200 OKstreaming response that carries an error payload. Request tokenization and the handoff to the model worker now complete before the streaming response headers are sent, so a failure at submission time (for example, a dead model worker) maps to an HTTP 5xx (or 4xx for input errors). Errors that occur mid-stream, after the first chunk has been sent, are still serialized as an error event within the stream. -
Added request-queue backpressure to MAX serve via two cooperating caps. The
--max-queue-sizeflag (env varMAX_SERVE_MAX_QUEUE_SIZE, cap N) bounds the request queue to the model worker; once it is full, new requests are rejected immediately with HTTP 429 instead of being enqueued. The--max-pending-requestsflag (env varMAX_SERVE_MAX_PENDING_REQUESTS, cap M) stops the worker from draining the request queue once its pending (prefill) queue is M deep, so the request queue actually backs up under load. Together they form a self-calibrating mechanism that sheds load to keep latency within SLAs and naturally accounts for long requests holding batch space. Both default to unbounded. Rejections are observable via the existingmaxserve.request_countmetric withcode="429". -
Added
MAX_SERVE_GRACEFUL_SHUTDOWN_TIMEOUT_Sto control how long the server waits for in-flight requests to finish after receivingSIGTERMbefore exiting (default 5 seconds). Raise it so long-running requests are drained rather than dropped during a rolling restart. -
Added a request body size limit.
MAX_SERVE_MAX_REQUEST_BYTES(default 100 MiB) caps the size of an accepted HTTP request body; a larger request is rejected with HTTP 413 before the body is buffered, so a client cannot exhaust host memory with an oversized payload. The cap is enforced both from an oversizedContent-Lengthand by counting the bytes actually received, so a chunked or mislabeled body cannot evade it. Raise it for larger inline (base64) multimodal payloads, or set it to 0 to disable the limit. -
Data-parallel (DP) serving now shares the prefix cache across replicas, so a multi-turn conversation gets cache hits even when a later turn is scheduled on a different replica than the previous one. GPU prefix-cache hits are served by a cheap device-to-device copy of the cached pages onto the assigned replica, and the CPU/disk offload tiers are now a single pool shared by every replica (a block offloaded by one replica can be loaded by another). As a result,
host_offload_max_gbnow sizes one shared host pool of that size for the whole deployment, rather than allocating a separate pool of that size per replica. -
--kv-connector-config '{"type": "rust_tiered", "disk_offload_max_gb": 0}'now runs the tiered connector with no disk last level: offloaded blocks stop at the pinned host tier and no offload directory is created. Leavingdisk_offload_max_gbunset still sizes the disk tier from the device page pool, and a negative budget is now rejected instead of silently accepted. -
The dKV external KV-cache connector (
--kv-connector-config '{"type": "dkv"}') now supports data-parallel (DP) serving and shares its prefix cache across DP replicas on the default single-tenant path, matching thelocalandtieredconnectors. Every replica resolves to the same replica-agnostic store, and the stored block key carries no replica component, so a block offloaded through one replica is served to any other. -
The dKV external KV-cache connector now supports tensor parallelism (TP greater than 1) on the multi-tenant path for head-sharded (MHA/GQA), MLA (replicated-KV), and GQA head-replicated (
allow_kv_head_replication) models. Each GPU handshakes its own per-shard store, and every KV load/offload fans out across the processing replica's shard clients with identical block ids and hashes; a block counts as loaded only once every shard has it. The store key reflects the KV-head slice each GPU holds: the TP rank when head-sharded, a single shared shard for MLA, and the head-group index under head replication. -
On the dKV multi-tenant tensor-parallel path, a KV load that returns differing block counts across a replica's per-GPU shard clients now drains the over-loading shards' in-flight device reads before returning the minimum count. This keeps a stray in-flight host-to-device copy (into a block the block manager frees because it did not land on every shard) from later clobbering a reallocated block. The drain host-completes the reads on the remote (NIXL) transport and enqueues a cross-stream ordering on the co-located same-host (CUDA) transport, so it closes the window on both. The common equal-count path is unchanged and pays no extra synchronization.
-
The dKV external KV-cache connector (
--kv-connector-config '{"type": "dkv"}') now requires a non-empty tenant identity (MODULAR_DKV_TENANT_ID, set by the deployment operator); the empty-tenant "default" path is removed. Both the connector and the dKV server now reject an unset/empty tenant rather than keying an unfenced shared store, so every deployment (single-tenant included) routes through the per-tenant region-sharded store — DP replicas of one tenant still share one store. Multi-cache models (speculative draft+target, quantized values+scales) now resolve on this path, folded into the handshake'skv_config_hash. A single-tenant node spanning more than one GPU must set the dKV server's--fair-share-partitionsto its GPU count. -
The dKV external KV-cache connector now requires a NIXL transport (
MODULAR_NIXL_TRANSFER_BACKEND, one ofucx,libfabric, oruccl) and fails model load when it is unset, empty, orauto. dKV's auto-selection mode is removed: it activated the first discovered transport plugin, which is plugin-name order, so it resolved to libfabric on every host — including InfiniBand hosts, where UCX is the correct transport and the resulting configuration silently underperformed or failed at transfer time. Inferring the transport from what a host happens to have staged is not fixable by reordering, so the mode is gone rather than corrected. Every deployment that runs the dKV connector already sets the variable and is unaffected; a local or test run that relied on the default must now name a transport. The dKV server's--memxfer-backend/DKV_MEMXFER_BACKENDbecame required for the same reason, and it too rejectsauto. The failure mode differs by engine: MAX refuses to load the model, while mach logs the failure and serves on without the external KV tier, because a failed dKV bring-up is non-fatal there. -
The dKV external KV-cache connector now waits out a busy node instead of failing model load on it. dKV refuses a handshake when it has no room for another share, which is a transient condition that clears once a departing share's memory is released, so the refusal is now retriable and the connector's admission budget (
MODULAR_DKV_ADMISSION_TIMEOUT_S, default raised from 120s to 600s) retries it. A budget too small to cover several attempts is raised to that floor with a warning rather than rejected, so a deployment that pinned the old default keeps starting. -
A request's
dkv_cache_hintnow reaches the dKV external KV-cache connector, which reads it to load a cached prefix from the instance that holds it rather than only from the co-located one. The serving layer forwards the field without interpreting it, so the hint schema is versioned in one place and a hint this build cannot use costs a cache miss rather than a failed request. Previously the field was parsed into a form nothing read, and every hinted load went to the co-located dKV. -
The dKV external KV-cache connector now accepts a KV cache tree that mixes TP-replicated and head-sharded caches, instead of failing model load. Only an all-replicated tree produces a block that is byte-identical across TP shards, so a mixed tree offloads over the ordinary per-shard path. On that path a replicated cache is stored once per TP shard rather than once, so size the dKV share above what the
rust_tieredconnector needs for the same model. -
Added
MODULAR_MAX_RELEASE_FREE_HOST_MEMORY, an opt-in serving knob that returns free host-allocator pages to the OS once model compilation finishes, before graph capture. Graph compilation leaves tens of GiB free-but-unreturned in glibc's per-thread arenas, which glibc never reclaims on its own; setting this variable to any non-empty value callsmalloc_trim(0)at that point. On Gemma 4 31B this returns ~24 GiB of anonymous RSS per model worker in ~1.4s. Unset by default, and a no-op on platforms withoutmalloc_trim. -
Setting the
MODULAR_MAX_RELEASE_HOST_WEIGHTSenvironment variable to1frees the host copies of checkpoint weights once the GPU holds them, returning the full checkpoint size in host RSS. GPU deployments of graph-API architectures only; weights that execute on CPU must not be released. -
Chat completions now honor
reasoning_effort; previously only an explicitchat_template_kwargs.reasoning_efforthad any effect and the standard fields were silently ignored. An effort ofnonedisables thinking, and values set directly inchat_template_kwargsstill win. -
--num-speculative-tokensis now unset by default, and each speculative method resolves its own default:eagleandmtpkeep drafting 2 tokens per step, whiledflash-style block drafters (DFlash, DSpark) derive the draft checkpoint's trained block width. Explicit values are honored as before. Previously the flag defaulted to 2 for every method and block drafters overrode it at load time with a warning; a bare DFlash run now also sizes its KV cache draft headroom at the trained width instead of the old default. -
The vision encoder cache now stores embeddings in fixed-size blocks. Capacity is a byte budget carved into 128-token blocks — a video spans many blocks and an image a few — so a video-capable model no longer collapses the cache to a handful of worst-case-video slots that starve image workloads. The budget is set with the new
--vision-cache-utilizationflag, a fraction of the KV cache pool budget (default0.05;0disables caching). The previous entry-count cache and its--max-vision-cache-entriesflag are removed. -
Vision embedding assembly during chunked prefill is now bounded by the active window: each step copies only the embedding rows whose placeholder tokens fall inside the chunk, with dense scatter indices, instead of rebuilding every image's rows with out-of-bounds sentinels. Per-chunk copy cost now scales with the chunk size rather than the request's total image tokens.
-
Added
DeviceBuffer.unsafe_host_ptr()to the Mojomax.gpu.hostAPI. On devices with unified memory (Apple silicon), it returns a CPU-addressable pointer to the buffer, so the host can read a kernel's output afterDeviceContext.synchronize()without anenqueue_copyround trip. Reads through it are uncached, so it suits small control records rather than bulk readback. A CPU device returns the buffer's own pointer, since its allocations are host memory already; devices whose memory is not CPU-addressable raise. -
DeviceContext.create_event()andDeviceEventare now supported on Apple GPUs, backed byMTLSharedEvent. Event queries and waits track actual GPU completion instead of command-buffer submission order, and waiting on an event from another context's queue no longer blocks the host thread. -
DeviceContext.create_event()on NVIDIA GPUs now honors the defaultdisable_timingflag (previously inverted) and recycles events through the driver's event cache instead of growing it on every create/destroy cycle. -
Device-to-device copies on Apple GPUs no longer race when the source was written on another
DeviceStream. -
MODULAR_DEBUG=device-sync-modenow works on Apple GPUs, where it previously did nothing. -
Capturing
DeviceContext.enqueue_function()now encodes the closure throughDevicePassablebefore launch, matching explicit kernel arguments. Host handles such asDevicePointerreach the device as device addresses rather than raw host bytes. -
Added
max.nn.state_space.kda_decode, a wrapper over the Kimi Delta Attention recurrence op. -
Added
Graph.profile_scope, a context manager that labels every op for profiling. The scope name is appended to the op name in profile output. Profiler ranges are also created from sequential ops with the same scope, which is enabled withMODULAR_MAX_DEBUG_PROFILE_SCOPE_TRACING=1.
Inference server
-
/v1/responsesnow fetches client-suppliedinput_imageURLs through the same media resolver as/v1/chat/completions, so the two paths share one byte cap and one error mapping. Previously the responses path had its own downloader with no size limit, meaning an arbitrarily large image could be fetched and base64-expanded in memory, and its failures echoed the underlying network error back to the client. The inlineddata:URI's MIME type is now sniffed from the fetched bytes instead of guessed from the URL, and content that is not a decodable image is rejected with a 400 rather than inlined as an image. -
Structured-output grammars are now compiled once, in the model worker. The API server used to compile a
response_formatschema or tool-call grammar just to validate it, throw the result away, and leave the worker to compile the same grammar again against a cache it does not share. Removing the duplicate lowers time to first token for structured requests by 12-22% (Gemma 4 31B, concurrency 32); decode latency and requests without structured output are unchanged. An uncompilable grammar is still rejected with the same HTTP 400, streaming requests included, and a disaggregated prefill node now reports the failure to the decode node instead of leaving the request to time out. -
Speculative decoding can now verify only some of the draft tokens it generates, varying that count with the decode batch size via the new
num_speculative_tokens_per_batch_sizespeculative-config field. Each entry names an inclusive batch-size range and a count through the keysbatch_start,batch_end, andnum_tokens, so a two-range schedule is[{"batch_start": 1, "batch_end": 16, "num_tokens": 3}, {"batch_start": 17, "batch_end": 64, "num_tokens": 1}]. The first range must start at batch size 1 so every batch size resolves to a count; gaps and the tail carry the previous count forward. Drafting is cheap, but every draft the target verifies is another query position in its forward pass, so at high concurrency those positions compete with real tokens for the same compute and a rejected draft is compute spent for nothing. Whether narrowing pays off therefore depends on how well the drafts are being accepted, which is a property of the workload rather than of the batch size. Measure your own workload before adopting a schedule. The field is off by default, and unset behavior is unchanged. It applies to every speculative method. A block drafter (dflash) still drafts its whole checkpoint-fixed block every step, so a schedule narrows only how much of that block the target verifies; the saving comes from the target's verify pass, never from drafting less.It is most useful for a block drafter, whose draft depth is fixed by its checkpoint, making the verified count the only runtime lever on step cost. Where the draft depth is itself configurable, as it is for
eagleandmtp, loweringnum_speculative_tokensis the better tool: it removes the draft passes as well as the verify positions, while a schedule pays for drafts it then discards. A count of0is accepted and disables verification for that batch-size range. -
GLM models now map
reasoning_effortonto the two thinking levels their chat template can express, instead of forwarding it verbatim. The template reads onlyhighas a distinct level and treats every other value as maximum effort, so passing the value through inverted the scale:lowandmediumrequested maximum reasoning whilehighrequested less than they did. Every effort other thannone(which disables thinking),max(the template's own top level, still addressable directly) andxhigh(OpenRouter's name for that same top level) now selects the lower level, so an unrecognized value degrades to less reasoning instead of silently maxing out. Requests that set no effort are unaffected. -
Structured-output JSON grammars can be made whitespace-tolerant, per architecture via
default_structured_output_any_whitespace.- GLM 5 models default to whitespace-tolerant
response_formatgrammars.
- GLM 5 models default to whitespace-tolerant
-
Structured-output grammar compilation now runs off both serving hot paths. A new request's grammar matcher (from
response_formatJSON schemas or tool-call grammars) is built on a worker thread while the request waits for admission instead of on the scheduler's decode thread, and the API server's admission-time schema validation runs off the event loop instead of freezing in-flight streaming responses. A cold multi-second compile of a complex schema now delays only that request instead of stalling inter-token latency for every active request. -
A JSON schema that composes with
allOfis now enforced instead of refused.response_formatand tool-call schemas previously returned 400 for anyallOfwith more than one member, or with a sibling object keyword. The members now fold into one schema before compilation, including members nested in another member'sallOfand members that are a bare local$ref, so the common "shared definition plus an extension" shape compiles. A conjunction that cannot be folded exactly still returns 400 naming the keyword pair at fault, rather than compiling to a looser grammar. -
A JSON schema using
oneOfis now enforced when its branches can be proven pairwise disjoint, instead of being refused outright. Disjoint branches make the union exactly-one, which is whatoneOfmeans. Branch types andconst/enumvalue sets carry the proof, covering nullable values, scalar unions, enum partitions and unions discriminated by a constant property. A union that cannot be proven disjoint still returns 400, as does aconst/enumbranch carrying a keyword the lowering drops. The refusals apply when unsupported-schema rejection (reject_unsupported) is enabled. -
Fixed a union (
anyOf/oneOf) folding its sibling keywords into each branch too widely, which could accept values the schema forbids. These shapes now return 400 instead, when unsupported-schema rejection (reject_unsupported) is enabled: a closingadditionalProperties,itemsorunevaluatedPropertiesbeside a union, a base constraint beside$ref,constorenum— whether folded in from a union or written in the same object — and$refbeside a sibling union. -
Hardened the server-side fetch of client-supplied
image_url/video_urlreferences against SSRF: the host is now validated and hosts that resolve to internal or reserved addresses are rejected before the fetch. On by default (MAX_SERVE_MEDIA_URL_SSRF_PROTECTION_ENABLED); a per-host allowlist (MAX_SERVE_MEDIA_URL_ALLOWED_HOSTS, hostnames or CIDRs) permits trusted internal hosts. -
Compiling deeply nested JSON schemas is substantially faster and uses less memory by avoiding repeated subtree copies while constructing cache keys. Emitted grammars are unchanged.
-
Fixed strict JSON Schema compilation silently dropping string length bounds when a pattern or format is present. Redundant bounds now compile, while unsatisfiable or partially overlapping constraints return 400. Equivalent direct,
allOf, and union-folded schemas receive the same result. Regex length analysis has a per-schema work limit, so oversized patterns return 400 promptly. -
Fixed JSON Schema compilation resolving a local
$refagainst the wrong resource when the document declares a resource identifier ($id, oridin Draft 4) below its root. A fragment names a place inside the resource it is resolved against, and every fragment was resolved against the whole document, so a definition name that appeared in both an embedded resource and at the root bound the root's copy in silence. When unsupported-schema rejection (reject_unsupported) is enabled, such a document now returns 400 naming the declaration, rather than compiling a grammar the author never wrote. A document whose only resource identifier sits at the root, or that has none at all, is one resource and is unaffected. -
JSON Schema compilation now recognizes the
$schemadialects it models: Draft 4, whose resource identifier isid, and Drafts 6, 7, 2019-09 and 2020-12, whose identifier is$id. When unsupported-schema rejection (reject_unsupported) is enabled, any other$schemareturns 400 rather than being read as a modern document, because assuming the wrong dialect walks past the resources a document declares and resolves its fragments against the wrong one. Omitting$schema, as most tool schemas do, still means the current draft and is unaffected. -
Speculative decoding takes
--draft-proposal sampled(defaultargmax, unchanged). The draft model samples its proposal under the request's temperature/top-k/top-p and keeps the distribution it drew from, so verification runs true speculative sampling — accept on thep_target/q_draftratio, recover frommax(p_target - q_draft, 0)— rather than the typical-acceptance approximation, and the emitted tokens follow the target model's distribution.
Server metrics
maxserve_cache_hits_tokens_totalnow carries atierlabel naming what served each token:g0for the on-device prefix cache (including cross-replica device-to-device copies),externalfor the KV connector. The per-tier series sum to the untagged total, so an existing single-series query that doesn't group bytierreturns the same numbers as before. Misses stay unlabeled, which means a PromQL binary operation pairing hits against misses (a hit-rate expression) now matches on mismatched label sets and returns empty: addignoring(tier), or wrap the hits side insum without(tier) (...). The in-tree Datadog dashboard aggregates the tag away and is unaffected; external Prometheus consumers are the exposure. Previously the on-device share could only be derived by subtracting the external tier's own server-side counters, which measure what that tier holds rather than what a request could use and so overstate reuse. Note that the untagged series is replaced rather than extended, so arate()window spanning the upgrade sees the old series go stale and the labeled ones start from zero.- Added
maxserve_dkv_read_blocks_total, the count of KV blocks that landed in device memory from the dKV tier. Only confirmed-complete transfers count, so it measures delivered reuse. It is emitted only on dKV deployments, whilemaxserve_cache_hits_tokens_total{tier="external"}is stamped for any KV connector, so a missing counter means "not dKV" rather than "nothing landed". On a dKV deployment the two track each other for every load that lands, and comparing them needs the server's--kv-cache-page-size, since one is in blocks and the other in tokens. maxserve_dkv_rpc_read_latencyandmaxserve_dkv_rpc_acquire_latencynow report. Both were declared and published on a positive value, but nothing ever measured the underlying RPCs, so neither series ever appeared and the per-batch server log printedacquire 0.0ms, pin 0.0mson every line, which reads as an instant lookup rather than an unmeasured one. The connector now times both round trips. They bracket the RPC rather than the transfer, so they include work the transfer latencies cannot see, most importantly the disk-tier restage the server awaits inside its read handler.- Added
maxserve_dkv_nixl_read_latency_max, the slowest single dKV read in the window a batch samples, next to the existingmaxserve_dkv_nixl_read_latencyaverage. An average cannot separate one slow read from a uniformly slow batch, and it is the slow read that costs a request its time to first token. The peak also appears on the per-batch server log line and in the structured log, and it combines across data-parallel replicas by taking the maximum rather than by summing. - The per-batch dKV log clause now reports the blocks that landed and the bytes read, alongside the read average and the new peak. The block count was already in the structured log but missing from the human-readable line, and the byte count was not recoverable from either: the reported throughput divides by the transfer-time total, both surfaces carry only the average, and the sample count that bridges them is published nowhere. The clause is also emitted whenever a batch transferred blocks, where it was previously emitted only when a latency sample survived, so a read whose timing sample was dropped no longer drops the whole clause, and its block count with it. Such a batch reports its counts without the read timings rather than beside a row of zeros, which would read as an instant read.
- Fixed the speculative-decoding per-position acceptance-rate histogram
(
maxserve_spec_decode_acceptance_rate_per_position) understating acceptance: decode batches that performed zero verifications published a full row of 0% observations, diluting every position's average. Such batches now contribute nothing, matching the acceptance-length histogram's population. The batch log line also shows the acceptance length including the bonus token next to the accepted-drafts-per-step value, since the two conventions are easy to confuse.
max CLI
-
max warm-interpreter-cachenow shows a live progress row per op family. -
Fixed
max warm-interpreter-cachefailing with aValueErroron a machine where an op family supports none of the available devices (for example, a GPU-only op family on a CPU-only machine). Such a family now warms as a no-op instead of aborting the whole command. -
Fixed LoRA and denoising-cache CLI flags replacing, rather than overriding, the matching
--config-filesection;--enable-lora=falsenow also disables LoRA that a recipe enabled, instead of being ignored.
Python API
-
max.nn.kernels.msa_sparse_attention_raggedandmsa_sparse_attention_ragged_mxfp8take a requiredsparse_block_size: the KV block size in tokens from the model'ssparse_attention_config. It must equal the KV cache page size, and the kernel now asserts that rather than inferring a block size from the attention tile-width default. -
max.experimental.nn.Module.compilereuses precompiled MEFs when the session has them, so a ModuleV3 model can be compiled where no accelerator is attached and initialized where one is.max.experimental.support.set_export_mefsrecords each compiled graph into a directory, andmax.experimental.support.set_precompiled_mefsinitializes those artifacts instead of compiling.InferenceSession.compile_reusing_mefsis the same half-step for callers that trace a graph and initialize it themselves. -
Eager mode tensors will use the JIT by default. This unlocks fusion and shape specialization optimizations even for eager code, beating PyTorch performance in eager in the common case.
-
max.experimental.sharding.NamedMappingtakes its mesh from the enclosingmesh_context()when none is passed, so a layer can name the axis it shards along without being handed a mesh. Itsoriginal_specandoriginal_unreducedproperties are removed. -
Added
max.experimental.tree_utils, pytree utilities over nestedlist/tuple/namedtuple/dictand any class declaring the tree protocol:__tree_flatten__with either__tree_unflatten__or__tree_empty__, and an optional__tree_setattr__. There is no registry and no decorator, so a type opts in by declaring the methods.flattenandunflattencarry a value across a flat boundary,leaves,pathsandnodesread it,mapbuilds a new tree, andupdatewrites path-keyed values into an existing one in place. Every walk takesleaf, saying where it stops, andshared, saying whether a value reachable by two paths is one object or two. Import the module as a namespace:from max.experimental import tree_utils as tree. -
Added
max.experimental.compilation, three transforms over plain callables.stage(fn)(*args, **kwargs)tracesfninto amax.graphthat can be printed and inspected as MLIR. The arguments arefn's own, except that each tensor is given as aTensorType. This partially evaluatesfn: the tensor types become graph inputs, and every other argument is evaluated during tracing.compile(fn, weights=...)(*args, **kwargs)stages the same way and compiles the graph; the result is callable on real tensors. Weights and device memory load only on the first call, soexport_mefcan save the compiled graph to a file without loading either.as_subgraph(fn)returns a drop-in replacement forfnthat, during tracing, calls one shared subgraph instead of inlining its body, so a stack of identical layers compiles once. -
max.graph.ops.reduce_scatter_rms_normtakes an optionalgroup_sizeargument, matchingmax.graph.ops.reducescatter.sum: the devices split into contiguous groups of that many, each reducing independently, so the fused op also works under tensor-parallel-within-data-parallel topologies. It was previously full-world only and silently disabled itself whenever the tensor-parallel degree was smaller than the device count. -
max.graph.ops.allgather_rms_normtakes an optionalgroup_sizeargument, matchingmax.graph.ops.allgather: the devices split into contiguous groups of that many, each gathering independently, so the fused op also works under tensor-parallel-within-data-parallel topologies. It was previously full-world only. -
max.driver.Buffernow implements__str__, sostr(buffer)andprint(buffer)show the buffer's data formatted like a numpy array, followed by itsdtype,shape, anddevice.repr(buffer)still returns the metadata-only representation. -
Added
max.driver.Usage, an allocation-intent flag forBuffer.Buffer(..., usage=Usage.STAGING)requests host memory for staging transfers to and from the given device, which may be page-locked depending on the backend.Buffer.usagereports the intent;Buffer.pinnedreports whether the memory is page-locked. -
Breaking: the
pinned=argument toBuffer(...)andBuffer.zeros(...)is removed. Useusage=Usage.STAGINGinstead. -
DLPack export of a staging buffer (
__dlpack__, andto_numpy()in turn) does not synchronize pending device work. Synchronize explicitly before reading one after a device operation. -
max.nn.sampling.AcceptanceSamplerandmax.nn.sampling.stochastic_acceptance_samplertake adraft_proposalargument. The default,"argmax", is unchanged: the draft proposes deterministically and verification runs typical acceptance. With"sampled", the caller passes the distribution the draft sampled from, so verification runs the realp_target / q_draftratio test and recovers rejected positions frommax(p_target - q_draft, 0); temperature, top-k and top-p then all apply to the draft-verification distribution, where"argmax"applies only temperature. Sampled mode is GPU-only, needs a staticvocab_size, and cannot be combined with relaxed thinking-phase acceptance, whose rule assumes the drafted token is the draft's argmax. -
enable_dp_cross_replica_prefix_copynow takes effect on the Jenga KV cache, which previously logged that it was ignoring the flag. Under data parallelism a prefix cached on one replica is copied to the replica serving the request, in one batched device-to-device transfer, instead of being recomputed or fetched back through the host tier. The flag defaults to on, so this changes behaviour for every data-parallel deployment on this cache: on a multi-turn workload it cut time-to-first-token by around a quarter and left the host tier unused, at a cost in decode latency that shrinks as offered load rises. Set it to false to restore the previous behaviour.
MAX kernels
-
SM100 matmuls with an elementwise epilogue no longer leave output columns unwritten when
Nis not a multiple of 16, such asN=136orN=776. -
SM100 bf16 and fp8-input matmuls whose N leaves the output row stride short of TMA's 16-byte alignment, such as a 258-wide MoE router projection, now take the split-K GEMV at up to 64 rows instead of falling back to vendor BLAS.
-
The SM100 MLA decode dispatch now enumerates 12, 24 and 48 query heads alongside the powers of two it already covered, so a model whose per-device head count is not a power of two can bind its dispatch metadata.
-
KDA prefill now runs on the chunk-parallel pipeline. The pipeline existed as a Mojo kernel with no graph-op registration, so every prefill fell back to the token-sequential decode recurrence: O(total_seq_len) sequential steps per sequence, with no parallelism to spend on a long prompt. Registering
kda_chunkas its own graph op takes that to O(total_seq_len / CHUNK_SIZE). -
Added
MODULAR_APPLE_M5_ALLOW_LOSSY_F32_ATTENTION. Set it to0to keep fp32 attention off the Apple M5 MMA, which truncates operands to fp19. It defaults to the fast (lossy) path, matchingMODULAR_APPLE_M5_ALLOW_LOSSY_F32_MATMUL. -
Improved MXFP8 block-scaled matmul decode latency for attention output-projection shapes at M=4, M=32, M=64, and M=128 on MI355.
-
Improved MXFP8 block-scaled fused QKV projection decode latency at M=4 on MI355.
-
The MLA sparse-attention indexer (DeepSeek V3.2, GLM 5.x) now does work proportional to each row's actual key count instead of the batch's
max_cache_lengthmetadata. Inside captured decode device graphs that metadata is baked at capture time — with a 1M-token maximum sequence length it sits orders of magnitude above the tokens a batch actually holds — and the indexer paid a full-width-infscore fill, a full-width top-k scan, and a key-tile-per-CTA scorer grid per layer per step at that frozen bound. The bitonic top-k kernels now clamp each row's scan to its live causal range, the score-buffer fill is skipped on the SM100 scorer path (which writes every live slot itself), and the SM100 scorer's key-split route now covers the tensor-parallel head counts (4 and 8) with its part count capped at a fixed number of waves, so the grid is sized to the hardware rather than to the metadata bound while per-CTA loop bounds come from the runtime cache lengths. At the GLM 5.2 MTP decode shape (batch 8, width 6, 76k-token context, 4 heads per rank) with metadata frozen at 1M, one indexer layer drops from 0.89 ms to 0.10 ms on B200, matching its cost at a bound sized to the runtime lengths; shapes without a metadata gap are unchanged except a small fixed per-call cost for the row-bounds clamp (~4% on a batch-256, 4k-context decode). -
Sped up GPU token sampling by about 4% per output token when the largest
top_kin the batch is below 10, by removing a device synchronize fromfused_token_sampling_gpu. The synchronize backed a check that raised on an all-NaN logits row. Such a row now yields an arbitrary in-range token rather than an error. Setmax-debug.assert-leveltoallto restore the check, or usemax-debug.nan-checkto locate NaN logits. -
Fixed expert-parallel dispatch dropping half of every token belonging to an expert that only one communication SM serves, which surfaced as NaN logits. The block-scaled wire formats (NVFP4 and MXFP8) copy a token tile as two column halves claimed separately, and the claim loop stopped as soon as a claim covered the last token, so the remaining half was never copied unless a second SM happened to be on the same expert. Since experts are assigned round-robin over the communication SMs, this began once a device held more experts than half that count — 74 per device on a B200, so a 896-expert MoE over eight devices returned NaN while 512 experts stayed correct.
-
The SM100 grouped block-scaled matmul accepts MXFP4 weights against MXFP8 activations (W4A8), so a quantized MoE can feed its packed 4-bit experts straight to the tensor cores rather than dequantizing them to bfloat16 first. This removes MAX's per-forward
mxfp4_dequantover the routed expert stack, and it keeps the weights at their 4-bit footprint in global memory, which matters most at expert counts where a bfloat16 copy of the stack does not fit. A newunpack_fp4option on the NVIDIA TMA descriptor helpers, backed by theTensorMapDataType.PACKED_FP4_ALIGN16Btensor-map type, pads the weights into the byte-addressed form the tensor cores read as the copy engine lands them in shared memory. -
The joint top-k/top-p sampling kernel can now also return the masked, renormalized distribution it drew from, exposed as
max.nn.kernels.topk_fused_sampling_with_dist. Speculative decoding needs that distribution to build a rejection residual, and reads the sampled token's own probability out of it -- a value that has to agree with the sampler's accept decision, so it comes from the sampling kernel rather than a separate softmax. When top-k, top-p, and min-p are disabled, the distribution-producing path now skips its cutoff search. The existing single-output path is unchanged. On AMD GPUs, the distribution output also serves as temporary storage for exponentiated logits during sampling. -
MiniMax-M3 sampled MTP now samples only the accepted initial draft row instead of every possible acceptance position.
-
Added
max.nn.kernels.topk_topp_masked_probs, which computes a row's top-k/top-p masked renormalized softmax without sampling and without a sort. Speculative decoding verification reads the target's masked probability of each drafted token and builds its rejection residual from this one tensor, in the same form the draft sampler emits its proposal distribution. When top-k and top-p are disabled, the kernel now skips the cutoff search because every positive-probability token already survives. On AMD GPUs, it also caches exponentiated logits in the output buffer so cutoff-search passes do not recompute them. Rows with top-k disabled also omit positive-value counting from the initial mass reduction and cutoff search. -
Top-p-only distribution kernels bias cutoff-search pivots toward lower weights when the retained-mass budget is large relative to the mass still above the search's low bound, so the gain follows the bracket state rather than the requested
top_p. -
The fused gumbel-argmax sampling kernel takes a
from_probsparameter, exposed asmax.nn.kernels.gumbel_argmax_from_probs: each row's score isln(p) + gumbelover unnormalized probabilities, drawn with noise the kernel generates from a per-row seed. This enables sampling a speculative decoding rejection residualmax(p_target - q_draft, 0)that the caller builds in graph ops. GPU-only, non-Apple. -
Improved wide-row FP32 Gumbel sampling performance on AMD GPUs.
-
Retuned the MI355X dispatch table for a grouped block-scaled MoE matmul (gate-up and down projections) at the estimated-total-M > 2048 band that real serving traffic hits, plus the down projection's estimated-total-M <= 2048 band. Gate-up projection speeds up 7.4-10.1% and down projection 18.2-19.6% (etm > 2048) and 6.9-23.3% (etm <= 2048) across real ragged-M, skewed routing scenarios.
Breaking changes
-
Removed the
NPUdevice class frommax.driverand the correspondingDeviceRef.NPU(),DeviceRef.is_npu(), andDeviceKind.NPUfrommax.graph, along with theM_newNPUDevice()C API entry point.NPUwas a thin subclass ofAcceleratorthat differed only in the device label it stamped on the graph; it had no callers, and accelerator backends reached through a driver plugin are already served byAccelerator. ConstructAccelerator()(orDeviceRef.GPU()) for any non-CPU device, and read theAccelerator.apiproperty to tell the concrete backends apart. -
The tile-tensor storage policy is renamed to an engine, and the
layout.tensor_storagemodule is renamedlayout.tensor_engine. TheTensorStoragetrait becomesTensorEngine,TileTensor'sStorageparameter becomesEngine, and the conforming policiesPointerStorage,DevicePointerStorage, andStaticOffsetStoragebecomeDefaultEngine,DevicePointerEngine, andStaticOffsetEngine. The trait describes the operations a tile tensor performs on its handle (load, store, bitcast, elementwise) rather than the memory it points at, so the old name described the wrong thing. UpdateStorage=keyword arguments toEngine=and anytensor.Storageaccesses totensor.Engine. TheTensorOpstrait and the associatedStorageTypehandle keep their names, since they still describe the borrowed memory itself.Kernel signatures follow. Every comptime parameter bound to
TensorEngineorTensorOpsnow ends inEngine, replacing the three spellings that were in use:OutputStorageandXStoragebecomeOutputEngineandXEngine,QStorageTypeandSeedStorageTypebecomeQEngineandSeedEngine, and the snake_caseq_storageandx_storebecomeq_engineandx_engine. Callers passing any of these by keyword need to update the name. -
The KV connector's external host and disk tiers now report occupancy and transfer volume in bytes rather than in blocks. Those tiers are byte budgets the operator sizes in bytes (
host_offload_max_gb,disk_offload_max_gb), their block width need not match the device's, and bytes rate directly against PCIe and disk bandwidth. The device (G0) cache is unchanged and still reports blocks.KVConnectorreplaceshost_block_count/disk_block_countwithhost_byte_count/disk_byte_count, returning a newByteCount(the samefree/total/used/used_pct/free_pctsurface asBlockCount, measured in bytes). The KV cache managers make the same swap;block_count()is untouched.KVCacheMetricsrenamesh2d_blocks_copied,d2h_blocks_copied,disk_blocks_read, anddisk_blocks_writtentoh2d_bytes_copied,d2h_bytes_copied,disk_bytes_read, anddisk_bytes_written.The exported metrics follow:
maxserve.cache.h2d_blocks_copied,maxserve.cache.d2h_blocks_copied,maxserve.cache.disk_blocks_read, andmaxserve.cache.disk_blocks_writtenbecomeh2d_bytes_copied,d2h_bytes_copied,disk_bytes_read, anddisk_bytes_written, with unitbytes.maxserve.cache.used_host_kv_pctandmaxserve.cache.used_disk_kv_pctkeep their names and are now computed over bytes. Dashboards and alerts on the old tier counter names need updating. -
The pipeline configs are now immutable:
PipelineArgs,PipelineConfig,PipelineRuntimeConfig,SamplingConfig,MAXModelConfig,KVCacheConfigand its nestedKVConnectorConfig,LoRAConfig, andProfilingConfig. Assigning to a field after construction raises a pydanticValidationError. Construct them with the values you need. -
ModelManifestis now immutable from construction: mutating the mapping (item assignment,update,pop, and so on) raises aTypeError, andModelManifest.resolve()is removed — a manifest is complete when built. Construct it with the component configs you need. The unusedtotal_weights_sizeproperty is also removed. -
SpeculativeConfigis now immutable: assigning to a field after construction raises a pydanticValidationError. Construct it with the values you need. A failed speculative target-architecture rewrite now raises fromPipelineConfig.from_args()instead of being logged and ignored. -
An architecture can set
checkpoint_draft_widthon its registration to supply the draft width its checkpoint was trained for, so users of those models do not have to pass--num-speculative-tokens. A width that disagrees with the checkpoint is replaced, with a warning. -
Constructing a
MAXModelConfigdirectly now only validates the fields you pass. It no longer fills in the weight and model paths or loads the HuggingFace config. Configs the pipeline builds are unchanged. -
ArchConfig.calculate_max_seq_len()no longer takespipeline_config, andmodel_configis now required. -
KVCacheConfig.allow_kv_head_replication, the architecture registration fieldrequires_kv_head_replication, and the--allow-kv-head-replicationflag are removed. An architecture now asks for KV head replication in itsconstruct_kv_params(). -
The KV cache connector is now configured as a single object: its type moved onto
--kv-connector-configas atypefield, and the separate--kv-connectorflag is removed. Replace--kv-connector rust_tieredwith--kv-connector-config '{"type": "rust_tiered"}', and in a recipe setmodel.kv_cache.kv_connector_config.type.host_kvcache_swap_space_gbis renamedhost_offload_max_gbto matchdisk_offload_max_gb, and both now default to sizing their tier from the device page pool (1.5 times it on host, twice on disk) rather than to a fixed 50 GiB. Dict-valuedkv_cacheflags now merge field-wise over a config file's value instead of replacing it, so overriding one connector field on the command line keeps the rest -- previously a partial override reset the connector type and silently disabled offloading. -
Renamed
max.driver.DeviceStreamtoDeviceQueueandDevice.default_streamtoDevice.default_queue; the old names were removed. The driver models work submission as a command queue; a stream is one backend's implementation of that queue. Method, property, and argument names (Buffer.stream,stream=,native_stream_handle) are unchanged. -
Reworked
max.pipelines.PipelineArgsandPipelineConfigconstruction around a single path and a single (nested) shape:PipelineArgsnow nests its runtime, sampling, and profiling fields inruntime,sampling, andprofilingsub-configs (PipelineRuntimeConfig,SamplingConfig, andProfilingConfig), matching the nested shape already used by recipes andPipelineConfig. Flat constructor kwargs for those fields (for examplemax_batch_size=1) are rejected; passruntime=PipelineRuntimeConfig(max_batch_size=1)instead, and use the nested keys in config files validated intoPipelineArgs.PipelineArgs.from_flat_kwargs(the CLI path) still accepts the flat spellings and routes them to the sub-configs.- Removed
PipelineConfig.from_flat_kwargsandPipelineArgs.from_pipeline_config;PipelineConfig.from_argsis the single way to construct aPipelineConfigfrom user input. ReplacePipelineConfig.from_flat_kwargs(...)withPipelineConfig.from_args(PipelineArgs.from_flat_kwargs(...)). PipelineConfig.from_argsnow also applies the model generation config's sampling defaults, applies--model-overrideentries, and resolves the speculative draft architecture, so programmatically constructedPipelineArgsbehave the same as CLI invocations.PipelineRuntimeConfigis now exported frommax.pipelines.
-
--max-vision-cache-entriesis replaced by--vision-cache-utilization, a fraction of the KV cache pool budget for the vision encoder cache (default0.05;0disables caching). The cache is block-based, so an entry count no longer describes its capacity; configs setting the old flag must convert to a pool fraction. -
The legacy alias-buffer LoRA path has been removed. ModuleV3 LoRA (adapters passed as graph inputs) is now the only supported LoRA implementation. Serving a non-ModuleV3 architecture with
--lora-pathsnow raises a clear error at startup instead of building a manager that never applies the adapters; serve the model's ModuleV3 variant (for example,--prefer-module-v3) to use LoRA adapters. -
Removed the parametric
max.benchmark.bencher_iter_custom[fn](bencher, ctx)overloads and unusedbencher_iter_custom_multicontext(). Pass the launch closure as a value:bencher_iter_custom(bencher, fn, ctx). -
Removed
max.algorithm.reduce_boolean(), which took itsreduce_fnandcontinue_fnascapturingcompile-time parameters and had no callers. Usemax.algorithm.reduce()with a boolean accumulator, or write the early-exit loop directly. -
Removed the parametric
max.algorithm.parallelize[func](num_work_items, ...)andmax.algorithm.parallelize_over_rows[func](shape, axis, grain_size, ...)overloads that took acapturingclosure as a compile-time parameter. Pass the body as a unified closure in the first runtime argument instead:parallelize(func, num_work_items, ...)andparallelize_over_rows(func, shape, axis, grain_size, ...). Closure bodies drop@__parameter/@__copy_capturein favor of an explicit capture list, for exampledef body(start: Int, end: Int) {imm}:. -
Removed the parametric
max.algorithm.sync_parallelize[func](num_work_items, ...)overload that took acapturingclosure as a compile-time parameter. Pass the body as a unified closure in the first runtime argument instead:sync_parallelize(func, num_work_items, ...). The remaining overload acceptsdef(Int) raises -> None, so both raising and non-raising closures bind. Closure bodies drop@__parameter/@__copy_capturein favor of an explicit capture list, for exampledef body(i: Int) {imm}:. -
Removed the parametric
max.benchmark.bench_multicontext[fn](bench, ctxs, ...)overload. Pass the body as a unified closure in the second runtime argument:bench_multicontext(bench, fn, ctxs, ...). Nested closures passed this way drop@__parameterin favor of an explicit capture list such as{imm}or{mut buf, imm}. -
Removed the parametric
capturingoverloads ofDeviceContext.execution_time[fn](num_iters),DeviceContext.execution_time_iter[fn](num_iters), andDeviceContext.enqueue_cpu_function[fn](). Pass the closure as a runtime argument instead:execution_time(fn, num_iters),execution_time_iter(fn, num_iters), andenqueue_cpu_function(fn). Nested closures passed this way are unified closures, so replace@__parameterand@__copy_capture(x)with an explicit capture list such as{imm}or{var x, imm}. -
Removed the parametric capturing
layout.int_tuple.apply[func](t),reduce[reducer](t, initializer), and capturingapply_zip[func](...)overloads. Pass the closure as a runtime value:apply(t, func),reduce(t, initializer, reducer), andapply_zip(t1, t2, func)(orapply_zip(t1, t2, t3, func)). Nested closures passed this way are unified closures, so replace@__parameterwith an explicit capture list such as{}or{imm}. Thinapply_zip[func](t1, t2)function-pointer overloads are unchanged. -
DeviceGraphBuilder.add_function[kernel](*args, ...)takes a thin function pointer (func: def(...) thin -> None), the same identity asDeviceContext.compile_function[kernel](). -
PipelineRegistry.retrieve_factorynow returns aRetrievedPipelinedataclass withtokenizer,factory, andmemory_planfields instead of a(tokenizer, factory)tuple, so callers can reach the memory plan computed during retrieval. Replace tuple unpacking with attribute access.PipelineRegistry.retrieveis unchanged. -
The serving surface now reads the planned sequence length and batch token budget from the memory plan instead of re-reading them from the pipeline config.
TokenGenerationSchedulerConfig.from_pipeline_config,start_model_worker, the scheduler loaders, and the startup log helpers (log_basic_config,log_pipeline_info) take the memory plan as a parameter. Resolved values are unchanged. -
Renamed
MemoryPlan.max_lengthtoMemoryPlan.planned_max_lengthto distinguish the plan's value from the user intent onPipelineArgs.max_lengthand the construction-resolvedPipelineConfig.model.max_length, which keep their names. -
Denoising-cache input is now a frozen
DenoisingCacheSettingsonPipelineArgs(denoising_cache; in config files this section moves fromruntime.denoising_cacheto the top level). Construction fills unset fields from the architecture's TaylorSeer defaults into a frozenDenoisingCacheConfig. Enabling TaylorSeer without resolvable tuning fails at construction, as does enabling TaylorSeer and first-block caching together.
Fixes
-
Fixed an out-of-range
top_logprobskilling the model worker and taking the server down with it. The logprobs graph is built for a fixed top-k width and raises on a wider request, but the requested count reached it unvalidated, so a singletop_logprobsabove that width (or a negative one) exited the whole server process and left every later request without an endpoint. Both the chattop_logprobsand the legacy/v1/completionslogprobscount are now bounded at the request boundary and return HTTP 400 with the supported range. -
Fixed a pre-tokenized prompt longer than
--max-lengthkilling the model worker instead of being rejected. Only a string prompt was length-checked, so a token-array prompt — an OpenAI/v1/completionstoken array, or the pre-tokenized prompt an orchestrator supplies for KV cache-aware routing — was admitted at any length and produced a request whose length exceeded the model's context window. Under speculative decoding the response path then raised rather than capping, taking the worker down and wiping its prefix cache; such a request now returns HTTP 400 like an over-length string prompt. -
Fixed constrained decoding producing invalid output when combined with speculative decoding on AMD GPUs. The in-graph wait that gates the grammar bitmask copy was not recorded into captured device graphs, so replays read a stale mask.
-
Fixed tool calls being returned as raw markup in the assistant's
contentwhen a request did not declare atoolsarray. A tool established only by the conversation history, such as retrying a call that previously failed, now comes back as a structuredtool_callsentry. Parsing runs whenever the model has a tool parser configured;tool_choice="none"still opts out. -
Fixed
max-debug.source-tracebacks(for exampleMODULAR_DEBUG=source-tracebacksorGraph.debug.source_tracebacks = True) being silently ignored when it was enabled aftermax.graphwas first imported. The flag was cached at import time, so runtime error messages lacked theSource Tracebacksection pointing back at the Python code that built the failing op, even though the config reported the feature as enabled. -
Fixed the
disk_bytes_writtenKV cache metric counting blocks the tiered connector's disk tier declined to write because they were already saved or had a write pending. Re-offloading a block that had been evicted from the host tier but was still on disk inflated the count, and with it any disk-throughput figure derived from it. -
Fixed a
CUDA_ERROR_MISALIGNED_ADDRESScrash that could occur when concatenating tensors with byte sizes that do not satisfy the device's preferred alignment, including rank-1 index vectors with odd lengths. -
Fixed
generate_asyncraisingKeyError: Request ID not found in replica batchwhen requests in one batch finish on different steps, which happens whenever they are given differentmax_new_tokens. -
Fixed the offline
generate()andgenerate_async()APIs releasing only a finished request's KV cache blocks, and never the pipeline itself. The pipeline-level release is what frees a recurrent state pool slot and drops a request's vision encoder cache references, so architectures that carry recurrent state — such as Nemotron-H, Mamba, and LFM2 — leaked one state slot per request, and a second run in the same process could inherit the first run's slot and return different greedy tokens. -
Fixed
DeviceExternalFunctioncrashing on Metal instead of launching, so separately compiled kernels now load and launch there as they already did on other GPU backends. -
Fixed device buffer allocation no longer being pooled on GPUs without GPUDirect RDMA support, such as GeForce cards. A
DeviceContext.enqueue_create_buffer()create and destroy round trip on an affected device took roughly 67 us instead of 375 ns. -
Fixed abandoned image, video, and audio generation requests still being rendered. The scheduler these tasks share never read its cancellation queue, so a request whose client had disconnected was executed in full once it reached the front of the queue, and the cancellations themselves accumulated. A request cancelled before it starts is now dropped and answered as cancelled; one already in flight still runs to completion, since a render is a single uninterruptible call.
-
Fixed reductions over a zero-extent axis — for example
ops.sum(x, axis=1)where that axis has length0— leaving their output unwritten, along with anything fused into the reduction's epilogue. Each now writes its identity:0forsum,1forprod, the dtype's minimum formaxand its maximum formin, index0forargmaxandargmin, and NaN for floating-pointmean(asnumpy.meanreports). Integermeanreturns0. Note thatmax,min,argmax, andargminreturn an identity here rather than raising the way numpy does. -
Fixed
max benchmark --base-urlfailing before the first request against remote OpenAI-compatible endpoints: the server-readiness probe and the prefix-cache flush now target the--base-urlendpoint (instead ofhttp://<host>:<port>) and sendAuthorization: Bearer $OPENAI_API_KEY, matching the benchmark requests themselves. -
Fixed run-to-run nondeterminism of
layer_norm,rms_norm, and other Row-API rowwise reductions on Apple Silicon GPUs: a block that reduced several rows re-used its shared-memory strip across row iterations without ordering the trailing broadcast read against the next combine's first store. Model outputs on Metal (for example FLUX.2 image generation) are now byte-identical across runs; NVIDIA and AMD codegen is unchanged. -
On Apple Silicon, a missing Metal Toolchain (a separate download since Xcode 16) now surfaces
xcrun's own error, which names the fix (xcodebuild -downloadComponent MetalToolchain), instead of the opaque "Please submit a bug report." message. -
Fixed GPU discovery inside a container granted only MIG compute instances, which made MAX and Mojo unusable on MIG-sliced clusters. Discovery reported
GPU is not present, and a container holding several instances carved from the same GPU saw only one of them. Where NVML answers for the parent GPU, discovery now defers to CUDA, which describes the instance: for a device's memory when NVML rejects the query, and for the device count when MIG is enabled. (Issue #6896) -
Fixed tool-call requests failing with HTTP 400 (
anyOf branch and base schema both set "description") on models whose grammar compiles in strict mode (GLM-5.x, Gemma 4). The xgrammar JSON-schema converter'sanyOfbase-merge now skips annotation-only keywords (description,title,default,examples,$comment,deprecated,readOnly,writeOnly) instead of rejecting them as branch/base conflicts; they carry no grammar constraint. -
Fixed a race that enforced structured-output grammars during a reasoning model's thinking span.
-
Fixed structured output and constrained tool calling being silently ignored on the Kimi K2.5-family pipelines when serving with DFlash speculative decoding (
--speculative-method dflash). The unified DFlash graph compiled without the constrained-decoding bitmask inputs. The graph now binds the bitmask inputs and applies the grammar mask across every speculative position, matching the EAGLE speculative-decoding pipelines. -
Fixed GPT-OSS, OLMo 3, and OLMo 2 ignoring
--max-length. The server accepted prompts up to the checkpoint's own length limit, and sized the KV cache for that limit, whatever you asked for and whatever memory allowed. Both now use the length you set. Runs that pass no--max-lengthare unaffected. -
Fixed DeepSeek-V3.2 and GLM-5.x pipelines ignoring
--max-length: the resolved maximum sequence length was silently pinned to the DeepSeek default (163840) regardless of the flag or the checkpoint's advertised limit. These models also now size their rotary-embedding tables from the resolved maximum sequence length instead of the checkpoint'smax_position_embeddings. -
Fixed
ops.group_norm()raisingNotImplementedErrorin eager mode on CPU.group_normpreviously had a GPU-only kernel; it now has a CPU compute path too, so eagergroup_normruns on CPU the same waylayer_norm/rms_normalready do. -
Fixed the BF16 Expert Parallelism (EP) dispatch path failing to compile. The
ep.dispatch_asynckernel requires adispatch_scale_dtypecomptime parameter, but the BF16 branch ofcall_ep_dispatch_asynconly setdispatch_fmt_strand omitted the scale dtype, so any model using BF16 EP dispatch (for example, a non-quantized MoE) hit a graph-compile error. The BF16 branch now setsdispatch_scale_dtype = float32to match the kernel signature. -
Fixed CPU
argmax/argminreductions returning a wrong index for reduce axes of 256K+ elements, for example an argmax over a[1, 2097152]tensor, where the row's reduction fans out across multiple CPU workers. -
Fixed the distribution the top-k/top-p sampler emits for speculative decoding (
emit_dist) being under-normalized when amin_pmask removes weight and the row passes top-p at the first trial: the row was scaled by the unmasked softmax mass instead of the masked kept mass, so it summed to less than one and skewed the rejection residual. The sampled token stream was and remains unchanged. -
Fixed a model worker crash when constrained decoding and speculative decoding were enabled together. A batch at the prefill-to-decode boundary verifies no drafts, which the grammar bitmask fill rejected.
Mojo language
For all the updates to the Mojo language, standard library, and tools, see the Mojo release notes.