Skip to content

model : DeepSeek-V4.1-Flash-1017 full stack pin (all DeepSeek-V4.1 PRs except #292) - #298

Closed
danielhanchen wants to merge 71 commits into
masterfrom
dsv41-all
Closed

danielhanchen wants to merge 71 commits into
masterfrom
dsv41-all

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Single pin for DeepSeek-V4.1-Flash-1017: the full DeepSeek-V4.1 stack (#289 and its follow-ups) minus #292, so one entry in pr-set.json carries the whole model and the nightly merge stays additive.

Base is dsv41-pin-base61b = the pinned #61 commit (05a6693, IQ1_XS / IQ1_XXS / IQ1_XXXS as fork-local types 64..66) with upstream 797de63b2 merged in. Both #61 and this stack add a ggml_type and edit GGML_TYPE_COUNT, which no additive merge can resolve, so this pin carries the combined enum (MXFP8 = 43, IQ1_XS..IQ1_XXXS = 64..66, GGML_TYPE_COUNT = 67) and must sit after #61 in pr-set.json. Against that base the ggml.h and gguf-py/gguf/constants.py changes are pure additions. MXFP8 stays at 43 because the published DeepSeek-V4.1 GGUFs store the n-gram table as type 43.

Included:

Not included:

Built from the published PR heads with no other code changes: #297 head (which already carries #289 to #296 merged down the stack), one commit reverting the #292 diff, then #290, #291 and the #61 base merged (only conflict: the two type enums, resolved as their union).

Testing:

  • CPU build with LLAMA_FATAL_WARNINGS=ON, ctest -L main
  • CUDA build (sm_100) of llama-server, llama-cli, test-backend-ops
  • Pin-set merge simulation (llama_pinset.py sim, aged to newest tags) with this pin appended to the current pr-set.json

danielhanchen and others added 30 commits August 11, 2026 10:54
Three sub-IQ1_S data types: IQ1_S's algorithm with the codebook the mapping step may choose
from cut to a subset of iq1s_grid, and the index stored in fewer bits.

  IQ1_XS    id 64  46 B / 256  1024-entry grid  10-bit index  1.4375 bpw
  IQ1_XXS   id 65  42 B / 256   512-entry grid   9-bit index  1.3125 bpw
  IQ1_XXXS  id 66  38 B / 256   256-entry grid   8-bit index  1.1875 bpw

Dequantization is IQ1_S's rule throughout, y[j] = d*(2*ls + 1)*(grid[idx][j] + delta).
GGML_TYPE_COUNT is 67; 42..63 are left for upstream, which already uses 42 for Q2_0, so a
stock build rejects these files rather than misreading them.

CPU, CUDA dequant, MMVQ and MMQ. The MMQ path targets the current
ggml_cuda_mmq_util_funcs / ggml_cuda_mmq_config API and registers the types in every
per-architecture config table; Blackwell falls through to Ampere and needs none.

scripts/iqxs_gen_tables.py regenerates all six codebook tables in ggml-common.h
byte-identically from the three committed .npy grids, so the tables are reproducible rather
than pasted.

Tested on B200, CUDA 13, sm_100. test-backend-ops -o MUL_MAT passes 2/2 backends both
default and with GGML_CUDA_FORCE_MMQ=1, 11 cases per type matching IQ1_S. Note the default
run never enters the new MMQ kernels, since small n routes to MMVQ.
# Conflicts:
#	ggml/src/ggml-common.h
# Conflicts:
#	ggml/src/ggml-cuda/mmq.cuh
ggml-org#30168 made the src1 precision an explicit template argument of
mul_mat_q_case and removed its default.
…onverters

Mirror the IQ1_S CASE rows (same QK_K block, QR 8, Q8_0 SRAM layout) for
IQ1_XS, IQ1_XXS and IQ1_XXXS in mmq-config-gcn.cuh and
mmq-config-pascal-older.cuh, so MMQ selection on those arches no longer
finds zero configs and trips GGML_ASSERT(J_best > 0).

Add the three dequantizers to ggml_get_to_bf16_cuda to match the FP16/FP32
tables (cuBLAS BF16 compute path for quantized src0).

Map the three types in the loader's guessed-ftype switch.
FP8 32x32 linears are dequantized to BF16 (exact), routed experts are copied
bit-exact as MXFP4, Engram tables are precomputed as BF16 (e4m3 * 2^e8m0, exact)
and streamed in bounded chunks, router and Engram gate are kept in F32.
--outtype f32 keeps every dense tensor and the Engram tables in F32.

The Engram tables of all layers are written as one per_layer_token_embd (rows
concatenated in layer order) with the PLE keys of qwen4exp: ple.layers,
ple.ngram_size, ple.heads_per_ngram, ple.layer_multipliers, ple.head_offsets
(absolute rows), ple.head_vocab_sizes and embedding_length_per_layer_input.
The compressed token map and the pad token id stay deepseek41.engram keys.
…upport

- arch, KV keys and Engram tensors for deepseek41; the Engram tables of all layers are one per_layer_token_embd
  described by the qwen4exp PLE keys, the compressed token map and the pad token id are the only new Engram keys
- hparams for the cross-layer KV / indexer sharing and the two-level candidate selection
- DSV4 KV cache: compress ratios are taken from the slot state instead of the V4 constants; for V4.1 the CSA slot
  is ratio 2, the HCA slot ratio 1, without overlap and without the lightning-indexer cache, the index K is stored
  next to the compressed KV and only the KV source layers own rows
- the window cells keep the token ids (saved and restored with the state) for the Engram n-gram history
Engram (host-side n-gram hash, rows gathered on the CPU from per_layer_token_embd, which is read lazily from the
mapping like the qwen4exp n-gram table, its rows prefetched per ubatch), single-pass mHC, ratio 1/2 compressors with
cross-layer KV / index-K / top-k sharing and the two-level candidate block selection. Every compressed layer
attends densely over [window | all compressed rows] with the top-k applied as a mask. The indexer score,
candidate blocks and top-k are computed per chunk of query tokens, so the [index heads, tokens, rows]
intermediate stays near 2^26 elements at long context (a single chunk, the plain graph, whenever it fits).
Tokens shared by several sequences are supported; embedding input skips Engram, like the masked positions of
the reference. The model reuses the DeepSeek-V4 loader and graph helpers; the HC mix computation is split out
of build_hc_pre (unchanged for V4). The F32 router, HC and indexer matmuls request F32 precision, and the
router weights are normalized by sum + 1e-20 as in the reference. hash_layer_count is optional for V4.1: it has no
hash layers, and V4.1 GGUFs converted before the key was written lack it.
…d DSML tool calls

- test-llama-archs: a 6-layer config with window-only, Engram, ratio-2 and ratio-1 KV sources, top-k reuse and
  a candidate-masked index layer; the model saver writes the deepseek41 sharing and Engram keys (round trip)
- the official chat template (ported from encoding.py), template detection, and the DSML tool call parser for the
  V4.1 tag names (leading space: <|DSML| calls>, <|DSML| invoke>, <|DSML| parameter>)
- a V4.1 content continuation in thinking mode consumes the bare </think> of the generation prompt
- remove the V4.1 chat template file, its template detection, the DSML parser change and their test-chat
  cases; the converter drops the inherited DeepSeek-V4 default template (a template shipped with the model
  is still added by set_vocab)
- hash_layer_count is required again (the converter writes 0 for V4.1)
- the converter no longer imports sympy
- engram_index moves out of models.h; comments that restate the code are removed
For x < -16.6, 1.0f + expf(x) rounds to 1.0f in F32 and logf() returns
0, although softplus(x) ~= exp(x) is a normal float down to x ~= -87.
log1pf(expf(x)) keeps it.

DeepSeek-V4/V4.1 route with sqrt(softplus(logits)): when every selected
router logit is about -23 all expert weights became 0 and the token's
routed experts were dropped (the reference uses torch softplus).

Fix the CPU and CUDA SOFTPLUS ops and the fused CUDA sqrt-softplus
top-k MoE path. Add SOFTPLUS_RANGE to test-backend-ops.
The AMX buffer packs a single ne[0] x ne[1] slice, yet supports_op
accepted batched weights and ggml_backend_amx_mul_mat indexed src0 and
dst per batch, adding byte offsets to typed pointers.

- A 3D quantized weight (DeepSeek V4 attn_output_a, Q8_0) landed in the
  AMX buffer, AMX refused the permuted activation, the CPU backend
  cannot read AMX-packed data, and the scheduler aborted with
  GGML_ASSERT(*cur_backend_id != -1).
- A 2D weight times a batched src1 (DeepSeek V4 indexer attn_k) read
  past the packed weight and wrote dst at the wrong offset, crashing.

supports_op now requires a 2D src0, and the kernel treats all rows of
the contiguous src1/dst as one gemm. Unbatched multiplies are unchanged.
MXFP8 is the OCP microscaling FP8 format: 32 E4M3 values sharing one
E8M0 scale (33 bytes per block). DeepSeek-V4.1 stores its Engram tables
this way; keeping them as MXFP8 halves the table size against the exact
BF16 lookup (203 GB vs 393 GB) with identical dequantized values.

The type is gather-only: CPU and CUDA support GET_ROWS (source 0) and
refuse every other op in supports_op, so misuse fails at scheduling.
Row (de)quantizers use 64-bit block loops since the tables exceed 2^31
elements. gguf-py gains the type and a reference (de)quantizer;
test-backend-ops covers GET_ROWS.

MXFP8 takes the next free type id, 43.
The checkpoint's Engram tables are e4m3 rows with one e8m0 scale per 32
elements, i.e. MXFP8 block for block: copy them bit-exact as
[e8m0 | 32 x e4m3] blocks into per_layer_token_embd instead of
precomputing the BF16 lookup. NaN scale or code bytes are rejected.
DSV41_ENGRAM_TYPE=bf16 keeps the exact BF16 table (F32 with
--outtype f32).
… size

F32 x F32 MUL_MAT ran in exact F32 only for small batches; medium
batches used TF32 tensor cores (MMF) and large ones cuBLAS tensor-op
math, so results depended on the batch size even at GGML_PREC_F32.

When src1 is requested at GGML_PREC_F32 ([TAG_GGML_PREC]) and the
weights are F32, use the F32 matrix-vector kernel up to
MMVF_MAX_BATCH_SIZE columns, else cuBLAS SGEMM with CUBLAS_DEFAULT_MATH
(restored to CUBLAS_TF32_TENSOR_OP_MATH afterwards, as in solve_tri).
Other paths are unchanged.

test-backend-ops: MUL_MAT prec-F32 cases at NMSE 1e-10 across the
MMVF / MMF / cuBLAS ranges.
…put)

The radix top-k gather placed elements with atomicAdd slot counters, so
both the set of tied elements at the k-th key and the order of the
selected indices changed from run to run. Exact ties are common (masked
or ReLU-zeroed rows), and consumers that gather rows in top-k order then
accumulate in a different order every run.

Each block now owns a contiguous column chunk. A count pass records the
elements above and equal to the cut per chunk; the gather pass writes
them at the sum of the earlier chunks' counts plus an ordered in-block
prefix count. Each row holds the elements above the cut, then the ties
with the lowest column indices, both in ascending column order. The
shared atomic counter is gone.

test-backend-ops: TOP_K cases with fewer than k positive values and the
rest exactly 0.0 across the radix path.
llama-server --embeddings --pooling none toggles llama_set_embeddings
per request. With pooling NONE the toggle did not trigger a re-reserve,
so a model whose graph depends on cparams.embeddings (DeepSeek-V4.1
returns the flat HC state as t_embd) could run on a stale reservation,
and ggml_gallocr segfaulted on the first /completion. Re-reserve on
every toggle.
…ngram host placement

- Tokens shared by several sequences (llama-batched decodes the prompt
  once with n_seq_id = n_parallel) read the n-gram history of their
  first sequence. llama_kv_cache_dsv4::init_batch now checks that all
  of the token's sequences have the same predecessors (batch first,
  then the cache via the new llama_kv_cache::seq_pos_tok_le) and
  refuses the batch otherwise (decode returns 1, caches untouched).
- Mixed token/embd ubatches: embedding (image) positions hash as DEAD,
  get Engram gate 0, and their KV cells record LLAMA_TOKEN_NULL.
- The Engram table (per_layer_token_embd) stays in host memory under
  tensor overrides and split modes when it is not read lazily.
… engram_gate

- The compressed-attention path did not apply the Hadamard rotation
  that build_raw_attention applies to a quantized K cache. It now
  rotates q, the window K and the output like the window-only path, and
  rotates the compressed rows in F32 before storing them in the cache
  type for the concat.
- Quantized compressed K / index K views are dequantized directly to
  F32 (CUDA has no same-type copy for strided quantized views); with
  LLAMA_ATTN_ROT_DISABLE=1 they go through F32 as well.
- engram_gate is an elementwise weight: dequantize a quantized copy in
  the graph, and keep llama-quantize from quantizing it.

F16/BF16/F32 caches build the same graph as before.
…calls

Add the V4.1 chat template and detect it in common_chat. V4.1 keeps the
V4 DSML tool call format but its tag names carry a leading space, so the
DeepSeek parser handles both. The converter embeds the V4.1 template in
place of the V4 one. Add V4.1 parser cases to test-chat.
…parators

The V4.1 template reads keys the OpenAI message struct drops (task, per-message
tools, tool call namespace) and joins content blocks with blank lines around
images. Keep unknown message keys on common_chat_msg and restore them, with
media markers as separate text blocks, only for templates using the V4.1 DSML
tags. Fixes encoding.py golden cases 3-5 on the common_chat apply path.
…em-message tools

The V4.1 template only accepts low/high/max or 1-100 and raises otherwise, so
map minimal -> low, xhigh -> max and medium -> unset (template default) instead
of failing the request. Tools declared only on a system message now also feed
the tool call parser and grammar. Both are V4.1 only.
…er layer split

Layers read the compressed KV (and index K) of their kv source layer.
With -sm layer a source and its consumers can sit on different devices,
and the scheduler then copied the whole compressed-cache view across
devices once per consumer every ubatch (740 MB per decoded token at 64k
depth on 2 cards).

The first consumer on a device other than its source's now owns a
mirror cache layer in the same llama_kv_cache (shared cell bookkeeping,
seq ops and clears). The source's new compressed rows are appended to it
in that consumer's graph and every consumer on the device reads the
local copy. llama_dsv41_kv_read_layer() is the single definition used
by the cache and the graph. Compressor state stays with the source. No
change on one device or without KV offload; LLAMA_DSV41_KV_MIRROR=0
disables it.

The state format does not depend on the split: only kv source layers
are serialized and every mirror is refilled from its source on read, so
state saved under one split loads under any other and by a build
without mirrors.
…r, bounded FA, decode gather)

The reference graph attends over [window | every compressed row] with
the top-k as a mask, scores the indexer with an unfused chain over an
[n_idx_head, n_tokens, n_comp] F32 intermediate and passes n_kv_max = 0
to flash attention. With flash attention (F16 masks):

- the indexer uses ggml_lightning_indexer on the strided index-K view,
  scoring the ubatch as one chunk with the same top-k selection
- FA gets n_kv_max = window + top-k, so the CUDA sparse FA path runs
- ubatches with at most 32 queries per stream gather only each query's
  top-k compressed rows and attend over [window | selected] with one FA
  stream per query; keys and mask values match the dense path, only the
  key order changes. Gathered rows and masks are shared by all layers
  using the same top-k.

A quantized K cache (-ctk q8_0 / q4_0) takes the same paths. Without
flash attention nothing changes. LLAMA_DSV41_SPARSE=0 restores the
reference path (logits bitwise equal to it).
…PU graph splits)

The token embedding and the Engram table live in host memory, so each
GET_ROWS on them ran as its own CPU split between device splits (7
splits per decode token on 2 cards). The rows are now gathered and
converted to F32 on the host in set_input (same to_float / memcpy as the
CPU GET_ROWS, so bitwise identical) and enter the graph as F32 inputs,
leaving one split per device. build_inp_embd() gets an opt-in
host_gather flag that falls back to GET_ROWS when it cannot apply
(device-resident table, LoRA on the embedding, Hadamard-latent table,
mixed batches).

LLAMA_DSV41_HOST_GATHER=0 restores the in-graph GET_ROWS.
…ariant)

DeepSeek-V4.1 ships a 3-stage DSpark block drafter (mtp.0..2). It is
exported as a separate DFLASH GGUF with convert_hf_to_gguf.py --dspark and
used with --spec-type draft-dspark, like the V4 DSpark draft.

- DFLASH graph_dsv41: single-pass mHC (each sublayer collapses with the
  previous sublayer's pre-mix, output with the last FFN pre-mix, no
  hc_head), no per-head q norm, plain rope; markov and confidence heads
  as V4. Flagged by dflash.hyper_connection.single_pass.
- Block queries see the main-KV window up to the anchor plus the whole
  block; n_swa grows by one so the first query's mask row reaches
  window_size positions before the anchor.
- deepseek41 exports the target features (mean over the hc copies of the
  layer input) via embeddings_layer_inp.
- Converter: DeepseekV41DSparkModel (MXFP4 experts bit-exact, dense BF16,
  conf_proj F32; embeddings and lm head come from the target).
Reuse the DeepSeek-V4-Flash-Vision ViT + 3x3 aligner graph with the V4.1 token
block: [START] ([IMAGE]*w [NEWLINE])*h [END] in reading order, no lead pads,
no pad row, no image_pad tensor, causal decode. Port the V4.1 resize plan
(min_pixels upscale, single solve, 1024-token cap) and PIL ImageOps.pad
(round half to even), and round the normalized pixels to bf16 as load_image
does. Converter: DeepseekV41VisionModel writes the mmproj from the HF-style
vision_config.
im2col only reads the kernel shape. The zero-stride view was non-contiguous,
so CUDA rejected the op and the 3x3 unfold fell back to CPU. A contiguous
view of the same shape keeps it on the GPU; outputs are byte-identical.
Add the aligner shape to test-backend-ops.
@danielhanchen

Copy link
Copy Markdown
Member Author

Pushed 9930bb5, which merges three fixes from the leaf branches:

Net diff against the previous head is 3 files, +6/-2. CPU repros for each pass on this pin and on the full published stack; no-adapter logits are bit-identical. The pin-set repin from d1781c3 follows in a separate pr-set change.

@danielhanchen

Copy link
Copy Markdown
Member Author

Pushed 29b3b4b: namespaced DeepSeek V4.1 tool calls now parse. The template writes the tool name qualified with its namespace, but the parser built the expected tool-name literal from the bare function.name, so any tool defined under a namespace failed to parse. The parser now matches the qualified name the template emits (8 lines in common/parsers/deepseek.cpp). test-chat passes. The fix is merged down through #295, #296, #297 and #298 (dsv41-all be83657).

@oobabooga

Copy link
Copy Markdown
Member

Closing in favor of #332.

@oobabooga oobabooga closed this Oct 28, 2026
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.

2 participants