加载中…
加载中…
llama-server -m spark-ced-Q4_K_M.gguf -fa on -c 16384 -np 1 --jinja -ngl 99 (CED two-phase prefill, replay 1024; prompts under 1024 tokens do not show the prefill gain)在 LocalMaxxing 上查看测试
来自 LocalMaxxing 的 2 次社区测试(1 种配置)的中位数。
自己运行:配置指南XHToken/Spark-X2.6-4Bv1 lost a lot of long-context retrieval. The reason turned out to be the split point: the decoder's global layers (23, 27) need information that the layer-18 state simply doesn't contain yet (a closed-form fit from H_18 to the original layer-23/27 values leaves 37-48% error, and no projector training fixed it; from H_24 it's 6%). v2 moves the split to layer 24: layers 0-23 = encoder (all original weights, including global layer 23), layers 24-35 = decoder, whose 3 global layers (27, 31, 35) read K/V projected from RMSNorm(H_24).
v2 is not trained at all. The 6 projection matrices are a closed-form ridge-regression fit of the original model's K/V from H_24 (about 0.5M tokens of PG19 / code / chat / synthetic retrieval); there is no LoRA and every other weight is the original.
| v1 (split 18, trained) | v2 (split 24, no training) | original | |
|---|---|---|---|
| long-context generation, 138 items, official scorers: RULER (multi-key needles + variable tracking) | 69% | 88% | 90% |
| NoLiMa (36 items) | 8% | 31% | 33% |
| BABILong qa1-3 (54 items) | 52% | 43% | 50% |
| all 138 | 47% | 55% | 59% |
| KL(original ‖ CED), last 512 of 8K, replay 1024 (PG19 / code / chat, 85 held-out seqs) | 0.066 / 0.095 / 0.105 | 0.040 / 0.083 / 0.052 | 0 |
| top-1 agreement with the original | 0.885 / 0.922 / 0.889 | 0.896 / 0.937 / 0.913 | 1 |
| needles with 4 near-duplicate distractors, 20 trials at 4K / 8K / 16K | 19 / 18 / 20 | 20 / 20 / 20 | 20 / 20 / 20 |
| GSM8K, 100 questions, greedy | 87 | 85 | 89 |
| prefill speedup vs original, PyTorch reference path, replay 1024, 4K / 8K / 16K | 1.52 / 1.67 / 1.72x | 1.35 / 1.42 / 1.46x | 1x |
The trade: more of the model runs over the whole prompt (24 of 36 layers instead of 18), so prefill saves about a third instead of about half; in exchange retrieval comes most of the way back to the original. Decode speed is the same as the original either way. Long-context items are thinking-off, greedy, up to 32K tokens; RULER variable tracking is included with the answer prefix pre-filled as in the official setup. llama.cpp prefill on an Intel Arc Pro B70 (Vulkan, Q4_K_M, llama-server two-phase prefill with replay 1024, ub 512, warm; prompt of repeated token ids, timing only), tokens/s at 4K / 8K / 16K / 32K: original 2867 / 2503 / 1942 / 1332, v1 4234 / 4147 / 3535 / 2595 (1.48 / 1.66 / 1.82 / 1.95x), v2 3616 / 3358 / 2710 / 1907 (1.26 / 1.34 / 1.40 / 1.43x). The PyTorch speedups in the table use the same reference implementation for all variants (the GGUF parity is: mean KL 3.5e-4 to 2.3e-3 vs the PyTorch path, within about 2x of the plain-GGUF-vs-PyTorch noise floor on the same sequences).
v2 files: spark-ced24-Q8_0.gguf, spark-ced24-Q4_K_M.gguf (GGUF key spark2_5.ced.split = 24, same llama.cpp patch as v1, same bartowski imatrix), ced_weights_split24.safetensors (the 6 projections, bf16, 31 MB; demo.py reads the split from the file). The v1 files below are unchanged.
Experimental research artifact, not a drop-in model. Runs in PyTorch (
demo.py) and in a llama.cpp fork (GGUFs below); stock llama.cpp and vLLM cannot load it.
This takes the finished XHToken/Spark-X2.6-4B (36 layers, sliding-window layers with a global-attention layer every 4th layer) and converts it to the CED layout of DeepSeek-V4.1-Flash (arXiv 2609.19969, sec. 2.2 and 3.2.2) without pre-training anything from scratch:
Prefill runs the encoder (18 layers) over the whole prompt and the decoder (18 layers) over only the last replay tokens: close to half the prefill compute. Decode compute and the KV-cache size are unchanged (exact CED keeps one cache per layer; KV compression is CSA2's job, not CED's).
Only a small number of weights, by distillation against the original model (KL on its own logits + normalised per-layer hidden-state MSE; no labels, no teacher other than Spark itself), about 50M training tokens in total on one local B70 (~6 h):
Encoder layers 0-13 are bit-identical to the original. The LoRA can be folded into the weights (Spark.merge_lora), after which decode runs at the original's speed.
85 held-out 8192-token sequences (44 PG19, 24 code, 17 chat) never used for training or for choosing a checkpoint. KL(original || CED) on the last 512 positions, lower is better:
| variant | PG19 (replay 1024) | code | chat | PG19 (replay 512) | code | chat |
|---|---|---|---|---|---|---|
| naive init (the layers' own K/V weights on H_18) | 0.922 | 1.613 | 0.967 | 0.960 | 1.716 | 1.060 |
| closed-form ridge init | 0.214 | 0.492 | 0.301 | 0.269 | 0.561 | 0.387 |
| + trained K/V projections (26M) | 0.087 | 0.144 | 0.134 | 0.124 | 0.177 | 0.187 |
| + LoRA on the 18 decoder layers | 0.076 | 0.121 | 0.118 | 0.105 | 0.148 | 0.159 |
| final: + LoRA on encoder layers 14-17, + on-policy math/chat data | 0.066 | 0.095 | 0.105 | 0.093 | 0.122 | 0.141 |
| floor: original K/V in the decoder, replay only | 0.000 | 0.000 | 0.000 | 0.046 | 0.056 | 0.077 |
replay=1024.KL to the unquantized model for the usual GGUF quants (bartowski imatrix quants vs his bf16 GGUF, llama.cpp --kl-divergence on CPU, 4K-token chunks, same text domains) next to CED. Not the same positions as the CED numbers (CED: last 512 of 8K sequences), so read it as a rough placement:
| PG19 | code | chat | mean | |
|---|---|---|---|---|
| Q8_0 (GGUF, 4.38 GB) | 0.003 | 0.001 | 0.001 | 0.002 |
| Q6_K (GGUF, 3.61 GB) | 0.008 | 0.002 | 0.003 | 0.004 |
| Q5_K_M (GGUF, 3.16 GB) | 0.024 | 0.010 | 0.009 | 0.014 |
| Q4_K_M (GGUF, 2.62 GB) | 0.070 | 0.027 | 0.035 | 0.044 |
| IQ4_XS (GGUF, 2.34 GB) | 0.112 | 0.039 | 0.049 | 0.067 |
| CED (this repo) | 0.066 | 0.095 | 0.105 | 0.089 |
| IQ3_M (GGUF, 2.23 GB) | 0.248 | 0.096 | 0.104 | 0.149 |
| Q3_K_M (GGUF, 1.99 GB) | 0.302 | 0.117 | 0.156 | 0.192 |
| Q2_K (GGUF, 1.61 GB) | 0.912 | 0.420 | 0.617 | 0.650 |
| IQ2_M (GGUF, 1.57 GB) | 0.871 | 0.482 | 0.597 | 0.650 |
CED costs about as much as an IQ4_XS-to-IQ3_M quant. It saves prefill compute, not memory, and the two should stack (errors add up).
Greedy, chat template, no thinking, the same PyTorch code path for both: original 89/100, CED 87/100 (6 questions only the original solved, 4 only CED). A 2-question gap on 100 is within noise.
Prefill:
| prompt tokens | original | CED, replay 512 | CED, replay 1024 |
|---|---|---|---|
| 4,096 | 0.59 s | 0.35 s (1.67x) | 0.39 s (1.52x) |
| 8,192 | 1.27 s | 0.73 s (1.75x) | 0.76 s (1.67x) |
| 16,384 | 3.20 s | 1.73 s (1.85x) | 1.86 s (1.72x) |
The reference implementation is plain PyTorch with chunked attention, so absolute tokens/s are not comparable with optimised engines; the ratio is what carries over. The asymptote is ~2x. Decode at 8K context: original 52.4 ms/token, CED 52.9 ms/token (LoRA merged). CED is a prefill optimisation, not a decode one.
| prompt tokens | original | CED (replay 512) | CED (replay 1024) |
|---|---|---|---|
| 4,096 | 20/20 | 19/20 | 19/20 |
| 8,192 | 20/20 | 18/20 | 18/20 |
| 16,384 | 20/20 | 20/20 | 20/20 |
spark-ced-Q8_0.gguf (4.4 GB) and spark-ced-Q4_K_M.gguf (2.6 GB) have the LoRA merged in and carry the 10 K/V projections as extra tensors (blk.N.ced_k, ced_v; GGUF key spark2_5.ced.split). They need llama.cpp with the CED patch (llama.cpp-ced.patch in this repo, against upstream commit a0e6dc738; git apply it and build as usual, e.g. -DGGML_CUDA=ON or -DGGML_VULKAN=ON); stock llama.cpp refuses to load them (tensor-count error). The patch adds the CED graph, the converter and a llama_set_ced API and does nothing for models without CED tensors. llama-server -np 1 does the two-phase prefill by itself (replay 1024, LLAMA_CED_REPLAY=0 turns it off); other slot counts fall back to normal prefill. The quants use bartowski's imatrix, not one of my own.
Logits match the PyTorch CED within the bf16 noise floor (mean KL 3e-4 to 8e-4, CPU and Vulkan); with the decoder sliding-window mask deliberately removed the same test shows KL 0.34, so it does catch a broken graph. KL to the original bf16 GGUF (mean over PG19 / code / chat, llama.cpp --kl-divergence): CED bf16 0.067, CED Q8_0 0.068, CED Q4_K_M 0.114 (plain Q4_K_M: 0.044).
Prefill, tokens/s, Q4_K_M, warm, ub 512, -ngl 99, stock = XHToken's official Q4_K_M:
| prompt tokens | RTX 4090 stock | RTX 4090 CED | Arc Pro B70 (Vulkan) stock | B70 CED | ||
|---|---|---|---|---|---|---|
| 4,096 | 18.1k | 24.3k | 1.34x | 3.0k | 4.4k | 1.46x |
| 8,192 | 2.6k | 4.2k | 1.66x | |||
| 16,384 | 2.0k | 3.5k | 1.80x | |||
| 32,768 | 14.4k | 23.4k | 1.62x | 1.3k | 2.6k | 1.93x |
| 131,072 | 8.7k | 14.7k | 1.68x | |||
| 262,144 | 5.7k | 8.3k | 1.45x |
Decode (4090, tok/s at 4K / 32K / 128K context): stock 219 / 175 / 93, CED 226 / 177 / 94. The 4090 numbers are from a rented host with an i9-14900K; an earlier run on a slow server CPU was CPU-bound (GPU 48-64% busy) and showed almost no gain, so prefill speedups on a weak host will be smaller. The B70 numbers were measured on a build that also has my own Intel Xe2 Vulkan kernel changes (not in the patch here); the stock column is the same build without CED, so the ratio is CED's.
python demo.py /path/to/Spark-X2.6-4B ced_weights.safetensors --replay 1024 --compare
spark_ced.py is a self-contained PyTorch re-implementation of Spark (checked against the reference code and an fp32 CPU run) with the CED path. ced_weights.safetensors holds the 10 K/V projections and the LoRA deltas (bf16, 89 MB); the base weights come from the original repo. In code: proj, lora = load_ced_weights(...), S.merge_lora(lora), then S.ced_prefill(ids, proj, caches, tail=1024) and S.ced_step(...) with lora=None.
Made by @imdariotoo. I post my local-model releases on X, so follow there for the next ones.