Skip to content

Serve gemma-4-31b-4bit as flagship (fully resident) + fleet fixes; DeepSeek-V4 machinery parked in-tree#509

Draft
anupsv wants to merge 29 commits into
masterfrom
deepseek-v4-flash-4bit
Draft

Serve gemma-4-31b-4bit as flagship (fully resident) + fleet fixes; DeepSeek-V4 machinery parked in-tree#509
anupsv wants to merge 29 commits into
masterfrom
deepseek-v4-flash-4bit

Conversation

@anupsv

@anupsv anupsv commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Scope pivot (2026-07-05): the flagship serving target is now mlx-community/gemma-4-31b-4bit (18.4GB dense, fully resident) — DeepSeek-V4 is no longer the rollout target. The DSV4 machinery below stays in-tree, tested, and inert for non-DSV4 models (expert streaming, sequential serving, and DSML are all opt-in per model). Gemma 4 31B needs none of it: reference docs/reference/gemma-4-31b-serving.md, fleet admission tests coordinator/registry/gemma4_31b_resident_test.go (registration: default size, min_ram_gb=32).

What this PR ships, by relevance to the new flagship:

Applies to Gemma 4 31B (and the whole fleet):

  • NAX release posture: JIT-compiled NAX bf16 GEMM kernels are nondeterministic on M5 for ALL models (root-caused: AOT-compiled identical kernel source is clean; mlx-swift compiles jit_kernels.cpp) — releases build with -Xcc -DMLX_METAL_NO_NAX. See docs/spikes/nax-nondeterminism-m5.md + validated upstream issue draft.
  • Qwen3.5-VLM stale ropeDeltas crash 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. MLXVLM Gemma4 audited for the same pattern — clean. This unblocked E2E CI fleet-wide.
  • Fleet catalog catalog_size_gb override (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.
  • Cold-start expert-cache warming, capacity/cooldown merges from master, serving docs.

DSV4-specific (parked, in-tree, tested):

  • MoE expert SSD streaming (byte-budgeted LRU, UnifiedMemoryCap-derived budgets, purge-on-unload, usage-profile warming)
  • Sequential serving route for models the batched engine can't represent
  • Native DeepSeek prompt encoding + DSML tool-call parser
  • DeepSeek-V4 model port correctness fixes in mlx-swift-lm (submodule)

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"]
  end
Loading
flowchart 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"]
  end
Loading

Validation

  • Coordinator: go test ./registry/ -run TestGemma31b — admission across 32/36/48/64/96/128GB, 24GB rejection, cold-spill at floor. Full go test ./... green.
  • mlx-swift-lm: Gemma4 suites green; Qwen35 regression test (mlx-swift-lm#65) green; DSV4/streaming/tool suites unchanged and green.
  • Live on-box (M5 Max) Gemma 4 31B validation: load, single + batched decode, SSE, mid-decode admission waves — in progress, results will be appended.
  • (DSV4 historical validation retained in docs/reference/deepseek-v4-serving.md.)

Test plan

Made with Cursor

anupsv and others added 17 commits July 3, 2026 02:28
…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>
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
d-inference Ready Ready Preview Jul 6, 2026 6:30am
d-inference-console-ui-dev Ready Ready Preview Jul 6, 2026 6:30am
d-inference-landing Ready Ready Preview Jul 6, 2026 6:30am

Request Review

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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

  • TB-002 (Coordinator → Provider WebSocket) — server.go catalog size helper change
  • TB-003 (Provider operator vs process) — ProviderConfig.swift new config fields, BatchScheduler*.swift inference path changes
  • TB-007 (Provider inference engine) — entire BatchScheduler family
  • TB-009 (Apple attestation chain) — indirectly, via BatchScheduler.swift weight-hash reset path

Per-threat assessment

T-007 / T-012 / T-027 — Weight hash and model-integrity enforcement

ℹ️ Neutral on the weight-hash enforcement gap (SEC-007 remains open).

  • BatchScheduler+ModelLifecycle.swift: weightHash is still passed through loadModel and stored in currentWeightHash unchanged. The new expertStreamingCacheBytes / expertStreamingConfigured fields are reset in stopCurrentEngine (BatchScheduler.swift lines ~608–610), which also resets currentWeightHash — no regression there.
  • DeepseekV4TemplateFix.swift: model-type detection uses modelId.lowercased().replacingOccurrences(of: "-", with: "_").contains("deepseek_v4"). A model registered under a name that doesn't contain this substring will silently fall through to applyChatTemplate, which for a model with no Jinja template throws missingChatTemplate — that's a correct fail-loud path, not a silent bypass.
  • No change to the coordinator-side weight-hash admission logic; SEC-007 (fail-open on omitted hash) is untouched.

T-028 — Residual inference data in GPU memory

ℹ️ Neutral. The expert-streaming cache purge in stopCurrentEngine (BatchScheduler.swift ~+525–535) is a welcome addition: it releases the process-wide DeepseekV4ExpertStreaming.cache after all in-flight work is fenced. This reduces the window in which expert weight data sits in MLX cache memory between model unloads, but it does not address the open finding about GPU-buffer zeroing between consumer requests (the existing limitation for KV/activation buffers). No regression.

T-041 — Cross-tenant prefix-cache sharing / TTFT timing oracle

ℹ️ Neutral. BatchScheduler.swift changes don't touch prefixCacheManager or EncryptedPrefixCachePersistence. The new requiresSequentialServing path (effectiveMaxConcurrentRequests returns 1 for DeepSeek-V4) means prefix-cache sharing across concurrent tenants cannot occur for this model by construction — a minor implicit improvement, but not a code-level mitigation of SEC-035.

T-004 / T-024 — Admin/release key validation (server.go)

ℹ️ Neutral. The only change is SizeGB: catalogSizeGBForRow(row) replacing an inline expression. No auth, middleware, or key-comparison logic is touched.

T-021 / T-040 — MDM webhook / host-header injection (server.go)

ℹ️ Neutral. Same single-line catalog-size change; no webhook or URL-generation code is modified.

T-011 — X25519 key in config file (ProviderConfig.swift)

ℹ️ Neutral. Two new fields (streamExperts: Bool, expertCacheGb: Double) are added to BackendSettings. Neither is a secret; both use decodeIfPresent with safe defaults. No change to key storage or the existing config-file attack surface.


New attack surface not covered by existing threats

DeepseekV4TemplateFix string-match model dispatch (DeepseekV4TemplateFix.swift, BatchScheduler+Submit.swift ~L393–406, MultiModelBatchSchedulerEngine.swift ~L388–401)

The isDeepseekV4Hint check normalises hyphens to underscores and matches on substring "deepseek_v4" in either modelType or modelId. A malicious provider operator who registers a model with an ID containing "deepseek_v4" (e.g. "my-deepseek_v4-spoofed") would route all its prompts through DeepseekV4Encoding.encode instead of applyChatTemplate. This affects the provider-operator-vs-process boundary (TB-003): the operator controls model IDs in provider.toml and the local HuggingFace cache directory. The impact is prompt mis-formatting (potentially gibberish output or prompt injection via the encoding seam) for that provider's consumers — consistent with ADV-001's ability to serve manipulated outputs (T-007/T-012), but via a new code path. This is not a new class of threat, but the dispatch heuristic is fragile; a strict model_type == "deepseek_v4" equality check on the coordinator-verified catalog entry would be more appropriate than a substring match on operator-controlled strings.

Sequential-admission rejection leaks model identity (BatchScheduler+Submit.swift ~L100–109)

The error string "token_budget_exhausted: sequential-serving model is busy; retry shortly" is distinct from the standard token-budget error. If this string is forwarded to consumers via the coordinator's SSE error path, it leaks that the serving provider is running a sequential-serving model (i.e. DeepSeek-V4). This is a minor information-disclosure issue across TB-001 / TB-002. Worth checking that provider.go's inference error relay either redacts or normalises provider-side error strings before forwarding to consumers.


Open findings resolved by this PR

None. SEC-007, SEC-016, SEC-017, SEC-035 and other open findings are all unaffected.


🔐 Threat model: docs/threat-model.yaml · Updates on each push to this PR

@blacksmith-sh

blacksmith-sh Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Found 2 test failures on Blacksmith runners:

Failures

Test View Logs
github.com/eigeninference/d-inference/e2e/TestIntegration_ConcurrentRequests View Logs
github.com/eigeninference/d-inference/e2e/TestProfile_SingleProviderNonStreaming View Logs

Fix with Codesmith
Need help on this PR? Tag /codesmith with what you need.

…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>
@anupsv anupsv changed the title feat: DeepSeek-V4-Flash-4bit single-machine serving (SSD expert streaming + sequential route + DSML encoding) Serve gemma-4-31b-4bit as flagship (fully resident) + fleet fixes; DeepSeek-V4 machinery parked in-tree Jul 6, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant