Serve gemma-4-31b-4bit as flagship (fully resident) + fleet fixes; DeepSeek-V4 machinery parked in-tree#509
Serve gemma-4-31b-4bit as flagship (fully resident) + fleet fixes; DeepSeek-V4 machinery parked in-tree#509anupsv wants to merge 29 commits into
Conversation
…ls (DeepSeek-V4) The batched engine's cache factory silently substitutes BatchKVCache for custom cache classes (DeepseekV4LayerCache downcasts return nil -> garbage output, no crash). The load snapshot now classifies the model's cache layout via Scheduler.supportsBatchedServing; models that fail it serve EVERY request through the (generalized) single-sequence runner, exclusively, with the canonical retryable rejection when busy. submit(request:) delegates to the tokenized path for such models. Includes truncate-dsv4-checkpoint.py used for real-weights validation and the mlx-swift-lm submodule bump (DeepSeek-V4 port fixes: RoPE periods, clamped SwiGLU product, sinks, NaN sentinels, quantized wo_a, cache trim/copy safety + tests + DSV4Smoke). Co-authored-by: Cursor <cursoragent@cursor.com>
…e-path RNG seed (review P1-2) Co-authored-by: Cursor <cursoragent@cursor.com>
… fix (141GB DSV4 runs on 128GB via --stream-experts) Co-authored-by: Cursor <cursoragent@cursor.com>
…rt clamp regression test Co-authored-by: Cursor <cursoragent@cursor.com>
…rses (review P1-3) runGreedyFastPath drove the sequential (DeepSeek-V4) route through ModelContainer.generate, which attaches mlx-swift-lm's tool-aware text loop (TextToolTokenLoopHandler) — it can consume generated text into a .toolCall event, hard-failing tool-bearing sequential requests and risking a false-positive parse (and hard-fail) on plain-text ones too. The provider's own tool parsing already runs downstream on raw chunks (BatchedToolStreamHandler.processChunk), so the sequential runner must never tool-parse upstream. Add runSequentialRawTextPath, built on mlx-swift-lm's public generateTokens()/RawTokenLoopHandler (raw token IDs, no tool-call parsing) plus NaiveStreamingDetokenizer for incremental text — same admission/ first-token/finish bookkeeping, cancellation, and finish-reason semantics as runGreedyFastPath, minus any tool-call surface. No mlx-swift-lm changes needed; generateTokens, ModelContainer.perform(values:_:), and NaiveStreamingDetokenizer are all public API. submitTokenized now dispatches on fastPathRunnerKind(useSequential:): ALL sequential-serving requests (tool-bearing or not) take the raw runner; the Gemma B=1 greedy fast path keeps container.generate unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
…V4 MoE streaming Adds the backend config surface for mlx-swift-lm's DeepseekV4ExpertStreaming opt-in: stream_experts (default false) and expert_cache_gb (default 0 = auto-size). No wiring yet — just the config fields, coding keys, and default/round-trip/serialization test coverage following the existing kv_quant / adaptive_prefill pattern. Co-authored-by: Cursor <cursoragent@cursor.com>
…eaming ModelScanner's ordinary estimatedMemoryGb (on-disk bytes x 1.2) treats the ~125GB of routed-expert (switch_mlp) tensors as resident, which would refuse to load a 141GB DeepSeek-V4-Flash checkpoint on a 128GB box even though streaming makes the real resident footprint only ~16GB plus a bounded expert cache. - SafetensorsSizing: dependency-free (Foundation-only), header-only safetensors shard parsing that sums data_offsets deltas for tensor keys matching a predicate, without ever reading tensor payload bytes. - ExpertStreamingAdmission: pure arithmetic — residentWeightsGb (total minus switch_mlp bytes, times the same 1.2 overhead factor), auto-sized expert cache budget (physicalRAM - resident - 24GB headroom, clamped to [8, 70] GiB), and isSwitchMlpKey mirroring DeepseekV4Model's own shouldStreamWeight/sanitize predicate exactly. Both are Foundation-only (no MLX dependency), matching ProviderCoreFoundation's existing Linux-buildable design, and independently unit-tested without a real checkpoint on disk. Co-authored-by: Cursor <cursoragent@cursor.com>
…eepSeek-V4 Wires ExpertStreamingAdmission into ModelScanner.parseModelInfo: when this provider has stream_experts enabled AND a scanned model reports model_type == "deepseek_v4", estimatedMemoryGb is computed from the streaming-aware resident estimate (non-expert bytes + expert cache budget) instead of the naive full-footprint estimate — letting a ~141GB checkpoint admit on a 128GB box. Every other case (streaming off, non-deepseek_v4, missing hardwareInfo) stays byte-identical to the pre-existing estimate. No new wire fields: estimatedMemoryGb is an existing ModelInfo field this provider already reports to the coordinator, so the corrected value also improves the coordinator's routing/capacity signal for free. Threaded backend: BackendSettings through scanModels/scanAllModels/ parseModelInfo (default param, so untouched callers are unaffected) and updated the CLI call sites (start, doctor, models, picker) that already have ProviderConfig in scope to pass it. Co-authored-by: Cursor <cursoragent@cursor.com>
…+ KV budget ProviderLoop+ExpertStreaming (new): configureDeepseekV4ExpertStreamingIfNeeded runs before LLMModelFactory.shared.loadContainer — when stream_experts is on and the model's model_type == "deepseek_v4", sets DeepseekV4ExpertStreaming.enabled/.modelDirectory and sizes the expert cache budget via ExpertStreamingAdmission, surfaced to mlx-swift-lm through DSV4_EXPERT_CACHE_GB (setenv before the cache's first process-wide access — the only way to size it; DeepseekV4ExpertStreaming exposes no settable-budget API, a known limitation documented in the function's doc comment). Every load explicitly sets `enabled` (true or false), not just when true, so a prior streaming load can't leak into a subsequent non-streaming one. BatchScheduler: added expertStreamingCacheBytes (threaded from loadModel's caller). The expert cache is MLX-allocated but lives outside the model's registered parameter tree, so the load snapshot's measured `bytes` never counts it — applyPostLoadBudgets now folds snapshot.bytes + expertStreamingCacheBytes into ONE "weights" term before calling UnifiedMemoryCap.kvBudgetBytes, so the static token-budget ceiling set at load time doesn't assume headroom the cache will eventually claim. The live per-request KV gate needs no equivalent change — it reads real MLX active/cache counters, which already include the expert cache's bytes as they're allocated. Unit tests cover the byte-folding arithmetic (kvBudgetBytes with resident+ cache combined, saturatingAddBytes) and pin the non-streaming path (expertStreamingCacheBytes == 0) as byte-identical to before. Co-authored-by: Cursor <cursoragent@cursor.com>
…eterminism spike doc The darkbloom-local path inherited the config-aware (smaller) scanner estimate but loaded containers without configuring DeepseekV4ExpertStreaming — it would admit a 141GB streaming-sized model and then attempt a fully resident load. Extract ExpertStreamingConfigurator (shared by ProviderLoop and StandaloneServer), thread stream_experts/expert_cache_gb through StandaloneServerConfig, and configure before loadContainer. docs/spikes/nax-nondeterminism-m5.md: M5 NAX bf16-GEMM nondeterminism isolation, 10-second repro, fleet impact, mitigation options. Co-authored-by: Cursor <cursoragent@cursor.com>
DeepSeek-V4 ships no Jinja chat_template -- port the reference encoding_dsv4.py encoder (encode_messages/render_message/render_tools/ encode_arguments_to_dsml) to Swift instead of failing with missingChatTemplate. - New DeepseekV4Encoding (ProviderCoreFoundation/DeepseekV4Encoding/): thinking vs chat mode, drop_thinking (auto-disabled when any message declares tools), the "## Tools" DSML system-prompt block, tool_calls -> <|DSML|invoke>/<|DSML|parameter> rendering, <tool_result> wrapping + reordering by originating tool_calls order, and the reasoning_effort=max prefix. Lives in ProviderCoreFoundation (not ProviderCore/Inference) since it's pure Foundation logic with no MLX dependency, and ProviderCoreFoundation can't depend on ProviderCore -- see DeepseekV4Encoding.swift's doc comment. - DeepseekV4TemplateFix (ProviderCore/Inference) hooks this in ahead of Tokenizer.applyChatTemplate at all three tokenization seams: MultiModelBatchSchedulerEngine.streamChatCompletion, .applyTemplate (/apply-template), and BatchScheduler.submit(request:). - TemplateRenderCheck's scan-time self-check now exercises the native encoder against canonical fixtures for deepseek_v4 models instead of reporting the vacuous "no template found" nil a template-less snapshot would otherwise produce, so template_render_ok keeps meaning "this provider can actually encode a tool-bearing request." - Golden-fixture tests port all 4 official encoding_dsv4.py test vectors (mirrored under Tests/ProviderCoreFoundationTests/Fixtures/dsv4/); 2 fixtures with no tool schemas match byte-for-byte, the 2 with tool schemas match byte-for-byte outside the schema JSON (which is compared semantically -- Swift's JSONValue/[String: Any] can't preserve the wire's original key order, so schema keys are sorted alphabetically for cross-run determinism instead; documented in DeepseekV4JSON). - Pins the existing "deepseek" prefix match in ProviderLoop.inferReasoningParser onto deepseek_v4 too (already correct; added a regression test) since DeepSeek-V4 completions start mid-think (reasoning</think>content) the same way DeepSeek-R1's does. Co-authored-by: Cursor <cursoragent@cursor.com>
…mp mlx-swift-lm pin effectiveMaxConcurrentRequests clamps to 1 when requiresSequentialServing — the submit gate admits one request at a time, so advertising the batched cap made the coordinator dispatch guaranteed-to-bounce concurrent requests. Submodule pin: expert-streaming perf (fd cache, conditional chunk eval, opt-in prefetch) + DSML tool-call parser (deepseek_v4 -> .dsml, parseMultiple hook) on top of the DeepSeek-V4 port fixes. Co-authored-by: Cursor <cursoragent@cursor.com>
…p, not physical RAM The auto-sizer subtracted headroom from PHYSICAL memory, ignoring the provider's own invariant (weights + KV + activations <= min(0.90 x physical, physical - 2GiB), UnifiedMemoryCap). It also floored the cache at 8GiB, which on small boxes pushed resident + cache PAST the cap - admitting a model the post-load serveability guard immediately unloads. Now: cache = cap - resident - activationReserve(planning accessor, env-aware) - 8GB KV target, clamped [0, 70]; an explicit expert_cache_gb is clamped to cap - resident - activations - 1GB min KV so a typo can't overcommit. Tests cover every fleet box size (36/48/64/96/128GB) proving resident + cache + activations + min KV <= cap. Co-authored-by: Cursor <cursoragent@cursor.com>
…a new API Nothing purged mlx-swift-lm's shared DeepseekV4ExpertStreaming.cache when a streaming DeepSeek-V4 model unloaded (idle timeout, explicit unload, or reload), so up to ~70 GB of cached expert weights stayed resident until process restart even though the rest of the model's weights were freed. The expert cache's byte budget was also only ever set via `setenv` + relying on the cache's lazy first-touch initialization, which silently no-oped on any load after the first in a process's lifetime. - BatchScheduler: add `expertStreamingConfigured` (set from THIS load's `ExpertStreamingConfigurator.configure` result, not derived from `expertStreamingCacheBytes > 0` or `requiresSequentialServing` — the latter is set for every DeepSeek-V4 load regardless of streaming, so it's correlated but independent). `stopCurrentEngine()` — the single teardown funnel for unload/idle-timeout/reload — now calls `DeepseekV4ExpertStreaming.purgeCache()` when set, AFTER the engine and fast-path tasks are fully fenced and BEFORE `MLX.Memory.clearCache()` so the freed expert arrays return to the OS in the same sweep. - ExpertStreamingConfigurator.configure now returns a `ConfigurationResult` (enabled + cacheBytes) instead of a bare byte count, and resizes the shared cache directly via the new `DeepseekV4ExpertStreaming .setCacheBudgetBytes(_:)` API on every call — a reload with a different configured `expert_cache_gb` now takes effect immediately, no process restart required. `setenv` is kept only for the DSV4Smoke CLI harness (a separate process). - StandaloneServer (`darkbloom local`): previously discarded the `ExpertStreamingConfigurator.configure` result entirely, so a streamed DeepSeek-V4 model loaded via the standalone path never told its scheduler `expertStreamingCacheBytes`/`expertStreamingConfigured` at all. Now threaded through `buildLoadedScheduler` so both the token-budget math and the unload-time purge work the same as the coordinator-served path. - Tests: `ExpertStreamingLifecycleTests` covers the pure enabled/disabled decision (streamExperts off, non-deepseek_v4 model_type, missing config.json, case sensitivity) and — using test-only setters plus the real, process-wide `DeepseekV4ExpertStreaming.cache` — that `unloadModel()` purges the cache iff `expertStreamingConfigured` was set, leaving an unrelated slot's cache alone when it wasn't. Depends on the mlx-swift-lm `ExpertCache.purgeAll()`/`setByteBudget(_:)` lifecycle API (submodule pin not bumped in this commit). Co-authored-by: Cursor <cursoragent@cursor.com>
…ndpoints A /v1/chat/completions request against a DeepSeek-V4 model only split reasoning_content from content when the request explicitly passed "reasoning_parser": "deepseek_v4" — without it, raw <think>...</think> text leaked into content. The coordinator WebSocket path (ProviderLoop+InferenceHandler) already worked around this by mutating the request directly before handing it to MLXOpenAIService, but the darkbloom local / --local-endpoint paths route straight from the Hummingbird router into MLXOpenAIService with no interception point. Wires the new mlx-swift-lm seam (MLXServerEngine.defaultReasoningParser( for:)) into MultiModelBatchSchedulerEngine: - registryProvider-based engines (coordinator WS path) resolve modelType from the registry snapshot directly. - Atomic-acquire engines (StandaloneServer / ProviderLoop+LocalEndpoint) resolve it via a new non-forcing `modelTypeProvider` closure, mirroring `tokenizerProvider` — never triggers a load or takes a reservation, since MLXOpenAIService only calls it after the model is already acquired for the in-flight request. - Both branches funnel through the existing `ProviderLoop .inferReasoningParser(for:)` model_type -> ReasoningParserFormat mapping, so deepseek_v4 (and every other family) resolve identically everywhere. An explicit request `reasoning_parser` still wins; models without a specific mapping keep their existing default. - Removed the now-redundant manual reasoningParser mutation in ProviderLoop+InferenceHandler — MLXOpenAIService resolves it via the engine uniformly. Tests: MultiModelBatchSchedulerEngineTests pins defaultReasoningParser resolution for both engine construction modes (deepseek_v4 and a non-DeepSeek model via registryProvider; deepseek_v4 via modelTypeProvider; nil when no model-type source is configured; the safe nil-modelType fallback for an unknown model id). Depends on the mlx-swift-lm MLXServerEngine.defaultReasoningParser(for:) seam (submodule pin not bumped in this commit). Co-authored-by: Cursor <cursoragent@cursor.com>
- release-swift.yml: build release binaries with -Xcc -DMLX_METAL_NO_NAX (M5 NAX bf16-GEMM nondeterminism; Gemma-4 A/B measured zero cost — docs/spikes/nax-nondeterminism-m5.md updated with measurements + decision) - submodule pin: ExpertCache purgeAll/setByteBudget lifecycle API + MLXServerEngine defaultReasoningParser seam Co-authored-by: Cursor <cursoragent@cursor.com>
…entries, NAX upstream issue draft Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This PR adds DeepSeek-V4 sequential-serving and MoE expert SSD-streaming support; it is primarily a feature diff with no meaningful change to the security posture — no mitigations are weakened or removed. Trust boundaries touched
Per-threat assessmentT-007 / T-012 / T-027 — Weight hash and model-integrity enforcement ℹ️ Neutral on the weight-hash enforcement gap (SEC-007 remains open).
T-028 — Residual inference data in GPU memory ℹ️ Neutral. The expert-streaming cache purge in T-041 — Cross-tenant prefix-cache sharing / TTFT timing oracle ℹ️ Neutral. T-004 / T-024 — Admin/release key validation ( ℹ️ Neutral. The only change is T-021 / T-040 — MDM webhook / host-header injection ( ℹ️ Neutral. Same single-line catalog-size change; no webhook or URL-generation code is modified. T-011 — X25519 key in config file ( ℹ️ Neutral. Two new fields ( New attack surface not covered by existing threats
The Sequential-admission rejection leaks model identity ( The error string Open findings resolved by this PRNone. SEC-007, SEC-016, SEC-017, SEC-035 and other open findings are all unaffected. 🔐 Threat model: |
…k-V4 Audited the coordinator-driven fleet path for MoE expert-SSD-streaming models: every memory gate (modelFitsHardware, reportedFreeForLoadAdmits, freeMemoryAdmits, coldTokenBudgetEstimate, and the identical cold-load predicate shared by TriggerModelSwaps/ColdSpillProviders/the warm-pool planner) assumes catalog size_gb approximates the resident weight. For DeepSeek-V4-Flash-4bit, SyncModelCatalog computed size_gb from the raw 141GB on-disk manifest total, which made the free-for-load gate reject a cold load on every fleet box (including the largest) and collapsed the servability predictor's cold token-budget estimate to zero. The audit found: (1) provider-reported ModelInfo.SizeBytes is never consulted by any coordinator gate today (only catalog size_gb/min_ram_gb are), so no protocol change is needed; (2) the model's true load-weight (~16GB, the non-switch_mlp on-disk tensor total) is box-invariant, unlike resident+cache, so a single global size_gb is correct as long as it represents load-weight, not footprint; (3) min_ram_gb already fully decouples structural fit from size_gb via modelFitsHardware's preference for it. Fix: catalogSizeGBForRow lets a model registry row's free-form runtime_parameters carry a "catalog_size_gb" override that SyncModelCatalog prefers over the on-disk total; every model without the override is byte-for-byte unchanged. Registration guidance (min_ram_gb=36, catalog_size_gb=16, context clamp, pricing) and the gate-by-gate audit table are documented in docs/reference/deepseek-v4-serving.md. Tests: registry/deepseek_v4_streaming_test.go covers admission across the 36-128GB fleet tiers, cold-load spill eligibility, PredictServable's warm/cold token-budget tiers, and single-slot concurrency queuing — each paired with a regression proving the raw on-disk size breaks the same gate. api/catalog_size_override_test.go covers the override end-to-end through SyncModelCatalog and the non-streaming byte-identical guarantee. Co-authored-by: Cursor <cursoragent@cursor.com>
… mlx-swift-lm#65) Fixes the provider broadcast_shapes crash under concurrent load that was failing E2E Integration Tests on this PR and on master. Co-authored-by: Cursor <cursoragent@cursor.com>
…eview Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…eference; park DSV4 docs gemma-4-31b-4bit (18.4GB dense, fully resident) needs none of the DSV4 machinery: no size override (raw manifest total is the resident footprint), no sequential serving (RotatingKVCache layout batches natively), no custom encoder (ChatML auto-injection handles the template-less base checkpoint). DSV4 code/tests/docs stay in tree but are no longer the rollout target. min_ram_gb=32 floor derived from the free-for-load gate math and pinned by tests across all fleet tiers. Co-authored-by: Cursor <cursoragent@cursor.com>
…ly available Co-authored-by: Cursor <cursoragent@cursor.com>
… and rollout runbook Scanner fixture pins the resident (non-streaming) estimate math and the provider-side memory-filter mirror of the coordinator's min_ram_gb=32 floor, plus vision_config detection for VLMModelFactory routing. Serving doc gains the engine-v2 posture (legacy engine until hardware parity/soak validation; env staging path documented) and the human-run rollout runbook (publish id conventions, registration params, pricing defaults, optional alias). Co-authored-by: Cursor <cursoragent@cursor.com>

Summary
What this PR ships, by relevance to the new flagship:
Applies to Gemma 4 31B (and the whole fleet):
jit_kernels.cpp) — releases build with-Xcc -DMLX_METAL_NO_NAX. Seedocs/spikes/nax-nondeterminism-m5.md+ validated upstream issue draft.ropeDeltascrash fix (E2E flake on master: provider WebSocket disconnect under concurrent load → all requests 502 #513, mlx-swift-lm#65): provider died under concurrent load; fixed + regression test. MLXVLMGemma4audited for the same pattern — clean. This unblocked E2E CI fleet-wide.catalog_size_gboverride (coordinator/api/catalog_size_override.go): registration-side correction for any model whose resident footprint differs from its on-disk size. Not needed by Gemma 4 31B (resident = on-disk), required by streaming models.DSV4-specific (parked, in-tree, tested):
UnifiedMemoryCap-derived budgets, purge-on-unload, usage-profile warming)Before / after
flowchart TB subgraph Before["Before: gemma-4-31b request"] A1[POST /v1/chat/completions] --> B1["model not in catalog / provider crash risk under concurrent load (#513)"] end subgraph After["After: gemma-4-31b request (32–128GB box)"] A2[POST /v1/chat/completions] --> B2["catalog: size_gb=18.4 (default derivation), min_ram_gb=32"] B2 --> C2["admission → load_model → fully-resident load via VLMModelFactory (Gemma4)"] C2 --> D2["continuous batching (RotatingKVCache) → ChatML-injected template → SSE"] endflowchart TB subgraph BeforeCode["Before: code"] E1["Qwen35.swift decode: repeated(ropeDeltas, batchSize) on stale state → broadcast trap, provider dies"] F1["SyncModelCatalog: SizeGB always raw on-disk total"] end subgraph AfterCode["After: code"] E2["Qwen35.swift: shape-total guard (ndim==1 && dim(0)==batchSize) + debug assert — mlx-swift-lm#65"] F2["catalogSizeGBForRow: runtime_parameters.catalog_size_gb override when present, else unchanged"] G2["gemma4_31b_resident_test.go: admission pinned across 32–128GB tiers with default registration"] endValidation
go test ./registry/ -run TestGemma31b— admission across 32/36/48/64/96/128GB, 24GB rejection, cold-spill at floor. Fullgo test ./...green.docs/reference/deepseek-v4-serving.md.)Test plan
min_ram_gb=32, no size override), pricing entry, provider releaseMade with Cursor