Skip to content

Vulkan DeepSeek MLA+MoE load OOMs with an unactionable OutOfMemoryException instead of preflighting the ~57 GB host F32 expert dequant #326

Description

@jamesburton

Summary

Loading a DeepSeek-V2-family GGUF (MLA + MoE) on Vulkan dies with a bare, unactionable

System.OutOfMemoryException : Insufficient memory to continue the execution of the program.
   at DotLLM.Models.Architectures.TransformerWeights.SliceExpertsToF32(...) TransformerWeights.cs:line 2291
   at DotLLM.Models.Architectures.TransformerWeights.LoadDeepSeekMoeLayer(...)
   at DotLLM.Models.Architectures.TransformerWeights.LoadMlaLayer(...)
   at DotLLM.Models.Architectures.TransformerWeights.LoadFromGguf(...)
   at DotLLM.Vulkan.VulkanTransformerModel.LoadFromGguf(...) VulkanTransformerModel.cs:line 1156

on a 128 GB box. Nothing in the message says why ~57 GB of host F32 was being allocated, or which tensors caused it. The footprint is knowable before a single byte is allocatedVulkanWeights.CanSkipMoeF32HostDequant already walks exactly the tensor descriptors that decide it.

Evidence — 2026-08-11 parity sweep

model result GGUF
deepseek-coder-v2-lite-q2_k FAIL / OOM in 112 s bartowski/DeepSeek-Coder-V2-Lite-Instruct-GGUF Q2_K, 6.43 GB
deepseek-v2-lite-q4_k_m FAIL / OOM in 144 s mradermacher/DeepSeek-V2-Lite-GGUF Q4_K_M, 10.36 GB

Both reach the Vulkan load only after the CPU reference model has already loaded successfully (54.5 s / 105.4 s), i.e. two ~57 GB host F32 expert copies are attempted, since the CPU path (TransformerModel.LoadFromGgufTransformerWeights.LoadFromGguf(gguf, config)) defaults skipF32MoeDequant: false. TransformerWeights.cs documents the size itself: "64 experts x 2048 hidden x 1408 intermediate x 3 mats x 4 bytes ~ 2.2 GB per layer x 26 MoE layers ~ 57 GB of F32 host RAM".

Root cause (verified against the actual GGUF tensor descriptors)

VulkanWeights.CanSkipMoeF32HostDequant returns false the moment any one routed bank on any MoE layer would fall back to F32, so every bank of every layer gets host-dequantised. MoeRoutedRawDeviceQuantType covers only Q8_0, Q4_K, Q5_K, Q6_K and (with coopmat) F16.

DeepSeek-V2-Lite's moe_intermediate_size is 1408, which is not a multiple of QK_K = 256, so llama.cpp is forced to quantise every ffn_down_exps bank to a non-K format. Census of the real files:

mradermacher/DeepSeek-V2-Lite-GGUF Q4_K_M (parsed with gguf.GGUFReader):

26 ffn_gate_exps.weight  Q4_K  (2048, 1408, 64)   <- supported
26 ffn_up_exps.weight    Q4_K  (2048, 1408, 64)   <- supported
14 ffn_down_exps.weight  Q5_0  (1408, 2048, 64)   <- NOT supported
12 ffn_down_exps.weight  Q8_0  (1408, 2048, 64)   <- supported

14 unsupported banks out of 78 force the other 64 to F32 too.

bartowski/DeepSeek-Coder-V2-Lite-Instruct-GGUF Q2_K (header range-fetched and parsed):

26 ffn_gate_exps.weight  Q2_K    (2048, 1408, 64)  <- NOT supported
26 ffn_up_exps.weight    Q2_K    (2048, 1408, 64)  <- NOT supported
26 ffn_down_exps.weight  IQ4_NL  (1408, 2048, 64)  <- NOT supported

78/78 unsupported.

This issue is the diagnostics half

The capacity limit itself is real (two 57 GB copies do not fit in 128 GB, and would not fit for full DeepSeek-V2 on any box). Closing the coverage gap so the weights stay quantised on device is tracked separately in #327.

What is a bug here is that we discover it by exhausting the machine's RAM and reporting a stack frame instead of preflighting a number we can compute for free.

Proposed fix

  • Extend the preflight to report why, not just whether: which layers/banks resolve to F32, their quant types, and the total F32 host footprint.
  • In VulkanTransformerModel.LoadFromGguf (both overloads), when the F32 fallback is taken and the computed footprint exceeds available memory, throw a typed exception (InsufficientMemoryException) carrying that itemisation — before allocating.
  • Gate on the footprint, not merely on the flag, so small MoE models that genuinely fit keep working unchanged.
  • Have RealGgufVulkanParityTests convert that typed exception into Skip.If with the full message, matching its existing VRAM-exhaustion skip. A capacity limit is not a parity failure, and reporting it as FAIL misrepresents sweep results.

Acceptance criteria

  • Both DeepSeek models produce an actionable message naming the offending banks/quants and the required host F32 bytes, instead of OutOfMemoryException.
  • The error is raised before any large allocation (fast, not after 112 s of dequant).
  • Unit test over the preflight itemisation using synthetic tensor descriptors (no GPU, no model file).
  • Parity sweep reports these two models as SKIP-with-reason, not FAIL.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions