🔥 antirez/ds4:面向 DeepSeek V4 Flash 与 PRO 的本地推理引擎
英文标题:🔥 antirez/ds4
DwarfStar(ds4)是一款小型原生本地推理引擎,优先针对 DeepSeek V4 Flash 优化,同时支持 GLM 5.2,以及超大内存设备上的 DeepSeek V4 PRO。项目支持 Metal、NVIDIA CUDA 和 ROCm,可通过 SSD 流式加载、多 GPU、张量并行和流水线并行,在消费级设备或旧款 CUDA 服务器上运行大模型。作者称,在 8 张 NVIDIA L40S 上测试多会话服务时,实现了 120 t/s 聚合生成速度和 2000 t/s 预填充速度;Mac M5 Max、M3 Ultra 和 DGX Spark 也给出了多项基准结果。该项目并非通用 GGUF 运行器,而是围绕特定模型、量化布局、工具调用、KV 状态、HTTP 服务和编码 Agent 联合构建的专用系统,目前仍处于快速迭代的 Beta 阶段。
63.1 KB
中文
DwarfStar 是一款小型原生推理引擎,首先针对 DeepSeek V4 Flash 进行了优化。它也支持 GLM 5.2,并可在超大内存机器上支持 DeepSeek V4 PRO。它是自包含且刻意聚焦的系统,而不是通用 GGUF 运行器。模型加载、提示词渲染、工具调用、KV 状态、HTTP 服务器和编码 Agent 都作为一个整体构建和测试。该仓库还包括用于 GGUF、imatrix、质量和速度测试的工具与数据。
支持的后端:
- Metal:主要目标平台,面向内存为 96 GB 或以上的 Mac。较小的机器可使用 SSD 流式加载。
- NVIDIA CUDA:包括多 GPU 系统和 DGX Spark。
- ROCm:适用于 Framework Desktop 等 Strix Halo 系统。
如果没有 llama.cpp 和 GGML,这个项目不会存在;请务必阅读致谢部分。我们向 Georgi Gerganov 以及所有其他贡献者致以诚挚感谢。
模型支持刻意采取机会主义策略。项目跟随适合实用本地设备内存规模的最佳开放权重,尤其面向 128 GB 笔记本电脑和 512 GB 工作站。当有更好的替代模型出现时,某个模型可能会被移除。
那么,我能用这款软件做什么?
-
你可以在消费级硬件上运行能力很强的模型,例如 MacBook、DGX Spark 或 Strix Halo。即使内存不足,也能通过 SSD 流式加载以相当不错的速度运行。
-
借助 CUDA 多 GPU 支持,以及 ds4-server 对解码和生成进行的微批处理,你可以将一台配备较旧 CUDA 显卡(Ada Lovelace 架构)、已不再获 vLLM 新模型支持的服务器,转变为面向公司的多用户 LLM 服务器。我们在配备 8 张 NVIDIA L40S 显卡的环境中测试了这一配置,多个会话下效果非常好:聚合生成速度为 120 t/s,预填充速度为 2000 t/s。
-
使用两台通过 RDMA 连接的 MacBook M5 Max / M3 Ultra,可以借助张量并行运行 4 bit DeepSeek Flash 或 GLM 5.2。
-
你还可以通过流水线并行将多个系统连接起来,汇聚其内存以运行更大的模型。
-
高端个人设备如今可以容纳能力强大的开放权重模型。
-
DeepSeek V4 Flash 和 PRO、GLM 5.2 能够承受激进的路由专家量化。
-
压缩 KV 缓存和快速本地 SSD 使长上下文变得实用。
-
这是一个专门服务于少数模型的推理系统构想。
-
本软件在 GPT 5.5、5.6、Claude Fable 的强力协助下开发,由人类主导创意、测试和调试。我们公开说明这一点,因为它影响了项目的构建方式。如果你不接受由 AI 开发的代码,那么这款软件不适合你。下文致谢同样重要:若没有主要由人工编写的
llama.cpp和 GGML,本项目不会存在。
致谢 llama.cpp 和 GGML
ds4.c 并未链接 GGML,但它得益于 llama.cpp 项目所开辟的道路,以及在其中发展出的内核、量化格式、GGUF 生态系统和来之不易的工程知识,才得以存在。我们感谢并亏欠 llama.cpp 及其贡献者。在构建这条面向 DeepSeek V4 的专用推理路径时,其实现、内核、测试和设计选择都是不可或缺的参考。一些源代码级组件根据 MIT 许可证被保留或改编于此:GGUF 量化布局与表格、CPU 量化/点积逻辑,以及某些内核。因此,也因为我们确实心怀感激,我们在 LICENSE 文件中保留了 GGML 作者的版权声明。
该软件目前变化非常快,请将其视为 Beta 质量。每次发布前都会进行大规模 QA 测试,但仍明确可能存在不稳定性。
如果你在寻找非常具体的内容,我们提供了其他子 README 文件;否则,如需正常使用,请继续阅读以下章节。
- CONTRIBUTING.md:供贡献者使用的正确性与速度回归测试指南。提交 pull request 前请先阅读此文件。
- QA_BEFORE_RELEASES.md:完整的发布测试矩阵,包括远程 Metal、CUDA 和 ROCm 机器。
- gguf-tools/README.md:离线 GGUF 生成、imatrix 收集、量化工具和质量检查。
- gguf-tools/imatrix/README.md:如何收集和使用 routed-MoE imatrix。
- gguf-tools/imatrix/dataset/README.md:如何生成校准提示词语料库。
- gguf-tools/quality-testing/README.md:如何针对官方 DeepSeek V4 Flash/PRO 续写结果,为本地 GGUF 打分。
- dir-steering/README.md:定向 steering 数据、向量生成及其使用方式。
- speed-bench/README.md:基准测试命令、图表和 CSV 生成。
- tests/test-vectors/README.md:用于回归检查的官方续写向量。
该实现仅适用于下方列出的 DeepSeek V4 和 GLM 5.2 GGUF 文件。它不是通用 GGUF 加载器;任意 GGUF 文件都不会具备该引擎预期的张量布局、量化组合、元数据或可选 MTP 状态。这里提供的 2 bit 量化已验证具有真正的高质量:表现稳定,可在编码 Agent 中使用,并能可靠地调用工具。
2 bit 量化采用非常不对称的量化方式:只有 routed MoE 专家会被量化,up/gate 使用 IQ2_XXS,down 使用 Q2_K。它们占据模型空间的绝大部分;其他组件(共享专家、投影、路由)则保持不变,以确保质量。
下载一个主模型。优先选择 imatrix 版本。
./download_model.sh q2-imatrix # 96/128 GB RAM machines, imatrix-tuned q2
./download_model.sh q2-q4-imatrix # 96/128 GB RAM machines, q2 with last 6 layers q4
./download_model.sh q4-imatrix # >= 256 GB RAM machines, imatrix-tuned q4
./download_model.sh pro-q2-imatrix # 512 GB RAM machines, PRO q2 imatrix quant
如需运行完整的 PRO Q4 分布式版本,请在每台机器上下载其中一半:
./download_model.sh pro-q4-layers00-30 # first half of PRO Q4 split
./download_model.sh pro-q4-layers31-output # second half of PRO Q4 split
该脚本从 https://huggingface.co/antirez/deepseek-v4-gguf 下载文件,将文件存储在 ./gguf/ 下,使用 curl -C - 恢复部分下载,并更新 ./ds4flash.gguf 以指向选定的主模型。pro-q4-layers00-30、pro-q4-layers31-output 和 pro-q4-split 目标会下载分布式 PRO Q4 组件,且不会更新 ./ds4flash.gguf。公开下载无需认证;但存在 --token TOKEN、HF_TOKEN 或本地 Hugging Face token 缓存时,将会使用它们。
如需重新生成 GGUF 文件或收集新的 imatrix,请参阅 gguf-tools/README.md。这些工具面向离线模型构建工作,在完整 DeepSeek V4 Flash 权重上可能耗时很长。本地工具支持 Flash GGUF 生成。PRO GGUF 的生产目前仍依赖基于外部 llama.cpp 的工作流;未来可以增加原生工具。
./download_model.sh mtp 会为 Flash 获取可选的投机解码支持 GGUF。它可与 q2-imatrix、q2-q4-imatrix 和 q4-imatrix 配合使用,但必须通过 --mtp 显式启用。当前 MTP/投机解码路径仍处于实验阶段:其受正确性门控,目前最多只能带来轻微加速,而非显著的生成速度提升。
GLM 5.2 支持仅限于该分支已测试的 GGUF 文件:
./download_model.sh glm-unsloth-q4 # Unsloth UD-Q4_K_XL, 11 shards
./download_model.sh glm-antirez-iq2xxs # antirez routed IQ2_XXS single-file GGUF
./download_model.sh glm-antirez-q2 # antirez routed Q2_K single-file GGUF
./download_model.sh glm-antirez-q4 # antirez routed Q4_K single-file GGUF
受支持的 GLM 布局会将 dense/model-control 张量保留在既有 Q8/F32 路径中,并支持 routed expert gate/up 张量使用 Q2_K、Q4_K 或 Q5_K;routed expert down 张量则支持 Q2_K、Q4_K、Q5_K 或 Q6_K。其他 GLM GGUF 量化布局在被有意加入并依据官方 100 例测试样本评分前,应视为不受支持。
这些格式并不都支持相同的执行模式。Q4 文件可用于常规 Metal 和 CUDA 推理。双 Mac 张量并行目前需要采用具备 ownership-aware 的 IQ2_XXS 或 Q2_K 路由布局;路由 Q4 GLM 必须在执行前被拒绝。
GLM 的 MTP 模块是主 GGUF 的一部分,不使用单独的 Flash MTP 文件。普通解码仍是默认模式。--glm-mtp 启用实验性贪心投机;--glm-mtp-timing 也会启用该功能,并打印接受率和时间统计:
./ds4 -m gguf/GLM-5.2-UD-IQ2_XXS_RoutedIQ2XXS_blk78Q2K.gguf \
--glm-mtp-timing --temp 0
GLM 推理使用 Metal、CUDA 或 ROCm 图后端。GLM 目前不支持定向 steering、低于 100 的 --power、显式 --prefill-chunk 以及外部 --mtp 文件。
然后构建:
make # macOS Metal
make cuda-spark # Linux CUDA, DGX Spark / GB10
make cuda-generic # Linux CUDA, other local CUDA GPUs
make strix-halo # Linux ROCm, AMD Strix Halo
make cpu # CPU-only diagnostics build
./ds4flash.gguf 是两个二进制程序使用的默认模型路径。传入 -m 可从 ./gguf/ 中选择另一个受支持的 GGUF。运行 ./ds4 --help 和 ./ds4-server --help 可查看完整参数列表。
DSpark 投机解码
DSpark 是 DeepSeek 为 DeepSeek V4 Flash 发布的辅助草稿模型。它从主模型读取隐藏状态,并最多提出未来五个 token。DwarfStar 使用主 Flash 模型检查这些提议,并仅提交被接受的前缀。主模型仍具有最终权威;被拒绝或低置信度的后缀会回退至普通目标解码。
其潜在收益是更快的生成速度:当多个候选 token 被接受时,一次目标验证即可让输出流推进多个 token。它不会加速预填充,草稿和验证工作也并非没有成本。可预测的续写,尤其是代码,通常获益最多;低收益提示词可能不会更快,甚至可能更慢。因此,DSpark 仍处于实验阶段,并且必须显式选择启用。
发布的 DSpark checkpoint 在这里被打包为一个约 5.6 GiB 的独立支持 GGUF。它不是独立模型。请下载一次:
./download_model.sh dspark-support
同一支持文件可与上面列出的 Flash q2-imatrix、q2-q4-imatrix 和 q4-imatrix 模型配合使用。目前不支持 DeepSeek V4 PRO。在 Metal 上,主模型可完全驻留内存,也可使用 --ssd-streaming;支持模型仍会增加其自身权重和运行状态的内存需求。DSpark 会在本次运行中替代旧版单阶段 MTP 支持模型,而非与其叠加使用。
使用贪心解码运行:
./ds4 -m ds4flash.gguf \
--mtp gguf/DeepSeek-V4-Flash-DSpark-support.gguf \
--dspark --temp 0
--mtp 提供支持 GGUF,而 --dspark 选择 DSpark 运行时。默认置信度阈值为 0.9;它会剪除不太可能补偿验证成本的后缀。--dspark-confidence 0 会强制采用固定的五 token 块,仅用于诊断。采样解码不会使用 DSpark 提议。--quality 和 --dspark-strict 同样保持仅使用目标模型的解码,这对比较和正确性检查很有用。
警告:由于优化工作提升了运行时速度,却没有同步更新基准结果,其中一些数字可能已不再更新。
以下为单次运行的 Metal CLI 数据,参数为 --ctx 32768、--nothink、贪心解码及 -n 256。短提示词是一个普通的简短意大利语故事提示词。长提示词用于测试分块预填充加长上下文解码。Q4 需要更大内存级别的机器,因此 M3 Max 的 Q4 数据为 N/A。
| 机器 | 量化 | 提示词 | 预填充 | 生成 |
|---|---|---|---|---|
| MacBook Pro M3 Max,128 GB | q2 | 短提示词 | 58.52 t/s | 26.68 t/s |
| MacBook Pro M3 Max,128 GB | q2 | 11709 tokens | 250.11 t/s | 21.47 t/s |
| MacBook Pro M3 Max,128 GB | q4 | 短提示词 | N/A | N/A |
| MacBook Pro M3 Max,128 GB | q4 | 长提示词 | N/A | N/A |
| MacBook Pro M5 Max,128 GB | q2 | 短提示词 | 87.25 t/s | 34.27 t/s |
| MacBook Pro M5 Max,128 GB | q2 | 11707 tokens | 463.44 t/s | 25.90 t/s |
| Mac Studio M3 Ultra,512 GB | q2 | 短提示词 | 84.43 t/s | 36.86 t/s |
| Mac Studio M3 Ultra,512 GB | q2 | 11709 tokens | 468.03 t/s | 27.39 t/s |
| Mac Studio M3 Ultra,512 GB | q4 | 短提示词 | 78.95 t/s | 35.50 t/s |
| Mac Studio M3 Ultra,512 GB | q4 | 12018 tokens | 448.82 t/s | 26.62 t/s |
| Mac Studio M3 Ultra,512 GB | PRO q2 | 32768 tokens | 138.82 t/s | 9.56 t/s |
| DGX Spark GB10,128 GB | q2 | 7047 tokens | 343.81 t/s | 13.75 t/s |
运行大于 RAM 的模型
常规 Metal 路径会尝试让模型驻留在 GPU 可寻址内存中。这是最快的路径,模型能够放入内存时,应始终将其作为默认选择。DwarfStar 还提供了 Metal 以及面向 GLM 5.2 的 ROCm SSD 流式加载容量模式。在该模式下,非路由模型
English / 原文
DwarfStar is a small native inference engine optimized first for DeepSeek V4 Flash. It also supports GLM 5.2 and, on very high-memory machines, DeepSeek V4 PRO. It is self-contained and deliberately narrow, not a general GGUF runner. Model loading, prompt rendering, tool calls, KV state, the HTTP server, and the coding agent are built and tested together. The repository also includes tools and data for GGUF, imatrix, quality, and speed.
Supported backends:
- Metal , the primary target, on Macs with 96 GB or more. Smaller machines can use SSD streaming.
- NVIDIA CUDA , including multi-GPU systems and DGX Spark.
- ROCm on Strix Halo systems such as the Framework Desktop.
This project would not exist without llama.cpp and GGML , make sure to read the acknowledgements section, a big thank you to Georgi Gerganov and all the other contributors.
Model support is intentionally opportunistic. The project follows the best open weights for useful local machine sizes, especially 128 GB laptops and 512 GB workstations. A model may be removed when a better replacement arrives.
So, what can I do with this software?
-
You can run a very capable models in your consumer hardware, a MacBook, a DGX Spark, or a Strix Halo for example. Even if you have not enough RAM, with SSD streaming, you can run it at a decent speed.
-
Using the CUDA multi-GPU support and with ds4-server micro batching of decoding and generation, you can turn a server with old-ish CUDA cards (Ada Lovelace architecture), no longer supported for new models by vLLM, into a multi-user LLM server for your company. We tested this setup with 8xL40S NVIDIA cards and multiple sessions with very good results. 120 t/s aggreated generation, 2000 t/s prefill.
-
Using two MacBook M5 Max / M3 Ultra RDMA, you can run 4 bit DeepSeek Flash or GLM 5.2 with tensor parallelism.
-
You can also use pipeline paralellism to glue together multiple systems to sum their RAM and run larger models.
-
Capable open-weight models now fit on high-end personal machines.
-
DeepSeek V4 Flash and PRO, GLM 5.2, tolerate aggressive routed-expert quantization.
-
Compressed KV caches and fast local SSDs make long contexts practical.
-
The idea of an inference system specialized for a few models.
-
This software is developed with strong assistance from GPT 5.5, 5.6, Claude Fable and with humans leading the ideas, testing, and debugging. We say this openly because it shaped how the project was built. If you are not happy with AI-developed code, this software is not for you. The acknowledgement below is equally important: this would not exist without
llama.cppand GGML, largely written by hand.
Acknowledgements to llama.cpp and GGML
ds4.c does not link against GGML, but it exists thanks to the path opened by the llama.cpp project and the kernels, quantization formats, GGUF ecosystem, and hard-won engineering knowledge developed there. We are thankful and indebted to llama.cpp and its contributors. Their implementation, kernels, tests, and design choices were an essential reference while building this DeepSeek V4 specific inference path. Some source-level pieces are retained or adapted here under the MIT license: GGUF quant layouts and tables, CPU quant/dot logic, and certain kernels. For this reason, and because we are genuinely grateful, we keep the GGML authors copyright notice in our LICENSE file.
The software is currently very fast changing. Consider it beta quality. Before each release, a big QA run is executed, however instabilities are definitely possible.
If you are looking for very specific things, we have other sub-README files. Otherwise for normal usage keep reading the next sections.
- CONTRIBUTING.md: correctness and speed regression testing guide for contributors. Read this before sending a pull request.
- QA_BEFORE_RELEASES.md: the complete release test matrix, including the remote Metal, CUDA, and ROCm machines.
- gguf-tools/README.md: offline GGUF generation, imatrix collection, quantization tooling, and quality checks.
- gguf-tools/imatrix/README.md: how the routed-MoE imatrix is collected and used.
- gguf-tools/imatrix/dataset/README.md: how the calibration prompt corpus is generated.
- gguf-tools/quality-testing/README.md: how local GGUFs are scored against official DeepSeek V4 Flash/PRO continuations.
- dir-steering/README.md: directional steering data, vector generation, and usage.
- speed-bench/README.md: benchmark commands, charts, and CSV generation.
- tests/test-vectors/README.md: official continuation vectors used for regression checks.
This implementation only works with the DeepSeek V4 and GLM 5.2 GGUFs listed below. It is not a general GGUF loader, and arbitrary GGUF files will not have the tensor layout, quantization mix, metadata, or optional MTP state expected by the engine. The 2 bit quantizations provided here are verified to be actually high quality: they behave well, work under coding agents, call tools in a reliable way.
The 2 bit quants use a very asymmetrical quantization: only the routed MoE experts are quantized, up/gate at IQ2_XXS, down at Q2_K. They are the majority of all the model space: the other components (shared experts, projections, routing) are left untouched to guarantee quality.
Download one main model. Prefer the imatrix versions.
./download_model.sh q2-imatrix # 96/128 GB RAM machines, imatrix-tuned q2
./download_model.sh q2-q4-imatrix # 96/128 GB RAM machines, q2 with last 6 layers q4
./download_model.sh q4-imatrix # >= 256 GB RAM machines, imatrix-tuned q4
./download_model.sh pro-q2-imatrix # 512 GB RAM machines, PRO q2 imatrix quant
For the full PRO Q4 distributed run, download one half on each machine:
./download_model.sh pro-q4-layers00-30 # first half of PRO Q4 split
./download_model.sh pro-q4-layers31-output # second half of PRO Q4 split
The script downloads from https://huggingface.co/antirez/deepseek-v4-gguf, stores files under ./gguf/, resumes partial downloads with curl -C -, and updates ./ds4flash.gguf to point at the selected main model. The pro-q4-layers00-30, pro-q4-layers31-output, and pro-q4-split targets download distributed PRO Q4 pieces and do not update ./ds4flash.gguf. Authentication is optional for public downloads, but --token TOKEN, HF_TOKEN, or the local Hugging Face token cache are used when present.
If you want to regenerate GGUF files or collect a new imatrix, see gguf-tools/README.md. Those tools are meant for offline model-building work and can take a long time on the full DeepSeek V4 Flash weights. Flash GGUF generation is supported by the local tools. PRO GGUF production currently still depends on the external llama.cpp-based workflow; native tooling can be added later.
./download_model.sh mtp fetches the optional speculative decoding support GGUF for Flash. It can be used with q2-imatrix, q2-q4-imatrix, and q4-imatrix, but must be enabled explicitly with --mtp. The current MTP/speculative decoding path is still experimental: it is correctness-gated and currently provides at most a slight speedup, not a meaningful generation-speed win.
GLM 5.2 support is limited to the GGUF files tested by this branch:
./download_model.sh glm-unsloth-q4 # Unsloth UD-Q4_K_XL, 11 shards
./download_model.sh glm-antirez-iq2xxs # antirez routed IQ2_XXS single-file GGUF
./download_model.sh glm-antirez-q2 # antirez routed Q2_K single-file GGUF
./download_model.sh glm-antirez-q4 # antirez routed Q4_K single-file GGUF
The supported GLM layout keeps dense/model-control tensors in the existing Q8/F32 paths and supports routed expert gate/up tensors in Q2_K, Q4_K, or Q5_K; routed expert down tensors are supported in Q2_K, Q4_K, Q5_K, or Q6_K. Other GLM GGUF quant layouts should be treated as unsupported until they are added deliberately and scored against the official 100-case fixture.
These formats do not all support the same execution modes. The Q4 files work for normal Metal and CUDA inference. Two-Mac tensor parallelism currently requires an ownership-aware IQ2_XXS or Q2_K routed layout; a routed Q4 GLM must be rejected before evaluation.
GLM's MTP block is part of the main GGUF; it does not use the separate Flash MTP file. Ordinary decode remains the default. --glm-mtp enables experimental greedy speculation. --glm-mtp-timing also enables it and prints acceptance and timing counters:
./ds4 -m gguf/GLM-5.2-UD-IQ2_XXS_RoutedIQ2XXS_blk78Q2K.gguf \
--glm-mtp-timing --temp 0
GLM inference uses the Metal, CUDA, or ROCm graph backend. Directional steering, --power below 100, an explicit --prefill-chunk, and the external --mtp file are not supported for GLM yet.
Then build:
make # macOS Metal
make cuda-spark # Linux CUDA, DGX Spark / GB10
make cuda-generic # Linux CUDA, other local CUDA GPUs
make strix-halo # Linux ROCm, AMD Strix Halo
make cpu # CPU-only diagnostics build
./ds4flash.gguf is the default model path used by both binaries. Pass -m to select another supported GGUF from ./gguf/. Run ./ds4 --help and ./ds4-server --help for the full flag list.
DSpark Speculative Decoding
DSpark is an auxiliary draft model released by DeepSeek for DeepSeek V4 Flash. It reads hidden states from the main model and proposes up to five future tokens. DwarfStar checks those proposals with the main Flash model and commits only the accepted prefix. The main model remains authoritative; a rejected or low-confidence suffix falls back to ordinary target decoding.
The possible gain is faster generation: when several proposed tokens are accepted, one target verification pass advances the stream by several tokens. It does not accelerate prefill, and the draft and verification work is not free. Predictable continuations, especially code, tend to benefit most; low-yield prompts can be no faster or even slower. DSpark is therefore still experimental and explicitly opt-in.
The released DSpark checkpoint is packaged here as a separate support GGUF of about 5.6 GiB. It is not a standalone model. Download it once:
./download_model.sh dspark-support
The same support file can be used with the Flash q2-imatrix, q2-q4-imatrix, and q4-imatrix models listed above. For now DeepSeek V4 PRO is not supported. On Metal, the main model may be resident or use --ssd-streaming; the support model still adds its own weights and runtime state to the memory requirement. DSpark replaces the legacy one-stage MTP support model for that run rather than stacking with it.
Run it with greedy decoding:
./ds4 -m ds4flash.gguf \
--mtp gguf/DeepSeek-V4-Flash-DSpark-support.gguf \
--dspark --temp 0
--mtp supplies the support GGUF, while --dspark selects the DSpark runtime. The default confidence threshold is 0.9; it prunes suffixes that are unlikely to repay their verification cost. --dspark-confidence 0 forces fixed five-token blocks and is intended for diagnostics. Sampled decoding does not use DSpark proposals. --quality and --dspark-strict also keep target-only decoding, which is useful for comparisons and correctness checks.
Warning: some of those numbers may no longer be updated, because of the optimization efforts that improved the runtime speed without updating the benchmark results.
These are single-run Metal CLI numbers with --ctx 32768, --nothink, greedy decoding, and -n 256. The short prompt is a normal small Italian story prompt. The long prompts exercise chunked prefill plus long-context decode. Q4 requires the larger-memory machine class, so M3 Max Q4 numbers are N/A.
| Machine | Quant | Prompt | Prefill | Generation |
|---|---|---|---|---|
| MacBook Pro M3 Max, 128 GB | q2 | short | 58.52 t/s | 26.68 t/s |
| MacBook Pro M3 Max, 128 GB | q2 | 11709 tokens | 250.11 t/s | 21.47 t/s |
| MacBook Pro M3 Max, 128 GB | q4 | short | N/A | N/A |
| MacBook Pro M3 Max, 128 GB | q4 | long | N/A | N/A |
| MacBook Pro M5 Max, 128 GB | q2 | short | 87.25 t/s | 34.27 t/s |
| MacBook Pro M5 Max, 128 GB | q2 | 11707 tokens | 463.44 t/s | 25.90 t/s |
| Mac Studio M3 Ultra, 512 GB | q2 | short | 84.43 t/s | 36.86 t/s |
| Mac Studio M3 Ultra, 512 GB | q2 | 11709 tokens | 468.03 t/s | 27.39 t/s |
| Mac Studio M3 Ultra, 512 GB | q4 | short | 78.95 t/s | 35.50 t/s |
| Mac Studio M3 Ultra, 512 GB | q4 | 12018 tokens | 448.82 t/s | 26.62 t/s |
| Mac Studio M3 Ultra, 512 GB | PRO q2 | 32768 tokens | 138.82 t/s | 9.56 t/s |
| DGX Spark GB10, 128 GB | q2 | 7047 tokens | 343.81 t/s | 13.75 t/s |
Running models larger than RAM
The normal Metal path tries to make the model resident in GPU-addressable memory. This is the fastest path and should remain your default when the model fits. DwarfStar also has an SSD streaming capacity mode on Metal and for GLM 5.2 on ROCm. In this mode the non-routed model weights stay resident, while routed MoE experts are kept in an in-memory cache and loaded from the GGUF file on cache misses.
Streaming is not as fast as fitting the full model in RAM. It still needs memory for non-routed weights, KV cache, graph scratch, activations, and the routed expert cache. It is useful because routed experts dominate model size and modern Mac SSDs are fast enough to make cache misses tolerable. Long prefills can still be fast; generation is more sensitive to cache misses because every new token routes through experts again.
Start with the automatic cache budget:
./ds4 -m ./ds4flash.gguf --ssd-streaming
If startup reports that the expert cache is too large, or if you want to reserve more memory for context, set the routed expert cache explicitly:
./ds4 -m ./ds4flash.gguf --ssd-streaming --ssd-streaming-cache-experts 32GB
The 32GB value is a routed-expert memory budget, not a generic byte cache. DwarfStar first reserves headroom for the two full routed layers used by overlapped streaming prefill, then converts the remaining bytes to the number of dynamic cached experts that fit for the current GGUF. Explicit NGB budgets may also be capped after context/KV accounting so the backend working set stays out of the slow pressure zone. A plain number such as --ssd-streaming-cache-experts 4000 is different: it means exactly 4000 dynamic expert slots, with no extra accounting. Non-routed weights, KV cache, graph scratch, and activations need additional memory. The automatic cache budget takes 80% of the backend's recommended working set, subtracts non-routed weights, then applies the same routed-prefill headroom before sizing the dynamic cache. Leave the hot expert preload enabled for normal use; use --ssd-streaming-cold and --ssd-streaming-preload-experts N only for measurements.
Practical SSD streaming examples
On 64GB MacBooks, start with the 2-bit Flash GGUF and a moderate expert cache:
./download_model.sh q2-imatrix
./ds4 \
-m ./ds4flash.gguf \
--ssd-streaming \
--ssd-streaming-cache-experts 32GB \
--ctx 32768 \
--nothink
On 128GB MacBooks, PRO q2 streaming is experimental but usable for inspection and occasional work when you accept slow generation. Start with --nothink:
./download_model.sh pro-q2-imatrix
./ds4 \
-m gguf/DeepSeek-V4-Pro-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-Instruct-imatrix.gguf \
--ssd-streaming \
--ctx 32768 \
--nothink
On an M5 Max with 128GB of RAM, a short PRO q2 streaming decode benchmark found the automatic budget best: it selected about 59GB of routed expert cache. Manual 64GB to 75GB caches were close on that machine. Prefer the automatic budget; if setting the cache manually on this class of machine, start around 48GB to 64GB, then increase only while the machine remains responsive and the startup log shows the requested dynamic cache. Once the machine is stable, re-enable thinking with a conservative generation limit:
./ds4 \
-m gguf/DeepSeek-V4-Pro-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-Instruct-imatrix.gguf \
--ssd-streaming \
--ctx 32768 \
--think \
--tokens 1500
GLM 5.2 uses the same option. Its streaming path keeps the largest full-layer prefix that fits resident, then uses the remaining budget for a dynamic expert cache. Start with the automatic budget:
./ds4 \
-m gguf/GLM-5.2-UD-IQ2_XXS_RoutedIQ2XXS_blk78Q2K.gguf \
--ssd-streaming \
--ctx 32768
The important startup line is the cache report. Start conservative, then increase the cache if the machine has headroom.
On a 128GB Strix Halo, use the routed Q2_K model and a 4096-token context as the starting point. The automatic cache budget leaves room for the GLM graph and KV state:
./download_model.sh glm-antirez-q2
make strix-halo
./ds4 --rocm -m gguf/GLM-5.2-UD-Q2_K_RoutedQ2K.gguf \
--ssd-streaming --ctx 4096
Distributed inference with pipeline parallelism
Pipeline parallelism lets DwarfStar run a model that is too large for one machine by splitting transformer layers across multiple machines. The main example is the full 4-bit Flash quant across two 128 GB MacBooks: each process maps only its own layer slice, activations are sent over TCP, and the coordinator keeps normal CLI/API behavior.
Pipeline parallelism can also speed up prefill by using multiple GPUs at the same time to process different micro-batches at different layers, like in an assembly line. Only prefill can be accelerated this way. Generation is purely autoregressive: each token must finish across the route before the next token can start. The model work is the same as a single process, plus coordination latency, so distributed generation is slower.
To build an initial mental model, here are the high level concepts:
- You put the GGUF on every machine, but each one loads just a subset.
--layerscontrols which tensors are mapped, so a worker with--layers 20:outputdoes not load the earlier layers. - Layer ranges are inclusive:
10:20means layers 10, 11, ..., 20.N:outputmeans layerNthrough the final layer plus the output head. - You assign one of the machines the role of
coordinator, the others the roles ofworkers. Workers will connect to the coordinator and will tell they are there and which layers they are able to process. - Each worker keeps its slice of the KV cache.
- Communication is worker-to-worker, there is no need to use the coordinator as relay, so if your coordinator is
A, and you make a request, activations will flow inA -> B -> C -> back to A.
How it works and how to configure it
The prefill path is pipelined (this is why it can go faster than in a single machine). For large prompts the coordinator can run its slice on chunk N+1 while the worker is running its slice on chunk N. The distributed rows below were measured with two M5 Max 128 GB MacBooks connected by Thunderbolt 5, using the Q4 Flash GGUF and the default 4096-token distributed prefill chunk. The single-process column is a reference run with the Q2 GGUF on a single machine, so it actually is a bit faster since the routed MoEs are smaller.
| Prompt | Single-process reference | Two MacBooks | Speedup |
|---|---|---|---|
| 9421 tokens | 421.70 t/s | 582.22 t/s | 1.38x |
| 28684 tokens | 405.30 t/s | 674.16 t/s | 1.66x |
| 63819 tokens | 353.62 t/s | 654.79 t/s | 1.85x |
Generation is different. It is strictly autoregressive : token N+1 cannot start until token N has produced logits and sampling has selected the next token. That means distributed generation cannot use the long prefill pipeline. It pays at least one cross-machine activation hop per generated token, so generation is slower than a single local process. On the same two-Mac Thunderbolt setup, a 12k-context control run with the 91 GB Flash quant went from 30.59 t/s single-process to 24.67 t/s distributed, a 19.4% loss. Distributed inference is therefore mainly for fitting larger models and speeding up long prefills, not for making decode faster.
Full DeepSeek V4 PRO Q4 on two Mac Studios
The full-size PRO Q4 GGUF can be run across two 512 GB Mac Studio M3 Ultra machines by giving the coordinator layers 0:30 and the worker 31:output. Use the split GGUF files so each side maps only the tensors it needs:
# Coordinator machine.
./download_model.sh pro-q4-layers00-30
# Worker machine.
./download_model.sh pro-q4-layers31-output
The two files are:
gguf/DeepSeek-V4-Pro-Q4K-Layers00-30.gguf
gguf/DeepSeek-V4-Pro-Q4K-Layers-31-output.gguf
This is a capacity use case: each process maps only its own half of the model, while the worker owns the output head and returns logits.
The current PRO Q4 Metal path uses queue-resident exact expert tables for the large routed experts. This avoids the broad multi-GiB routed-tensor bindings that made early distributed PRO Q4 attempts either run very slowly or hit Metal memory accounting limits. In a short greedy smoke test over the direct 192.168.0.182 / 192.168.0.183 link, the model generated coherent text and measured 11.47 t/s generation after startup. Per-token telemetry was balanced: local layers were around 39-43 ms, remote layers around 44-49 ms, for total token times around 84-92 ms. Expect a slow startup while each side maps and makes its half of the model resident. Long-context PRO Q4 prefill and decode performance still needs separate benchmarking.
The measurements above use a Thunderbolt 5 cable. The implementation is plain TCP and also works over slower links, including WiFi, but fast Ethernet or Thunderbolt networking is strongly recommended. Slow links mostly hurt generation latency and short prefills; large prefills can still benefit when the layer split is balanced. In the normal performance path, the last worker owns the output head and returns logits directly.
Minimal two-host configuration:
# Machine A: coordinator, owns tokenization, sampling, the prompt, and layers 0..30.
./ds4 \
-m gguf/DeepSeek-V4-Pro-Q4K-Layers00-30.gguf \
--role coordinator \
--layers 0:30 \
--listen 169.254.43.68 1234
# Machine B: worker, connects to A and owns layers 31..output.
./ds4 \
-m gguf/DeepSeek-V4-Pro-Q4K-Layers-31-output.gguf \
--role worker \
--layers 31:output \
--coordinator 169.254.43.68 1234
Normally the final worker should own the output head too, for example --layers 20:output. This avoids returning a full final hidden-state batch after prefill and lets the final worker produce the logits directly. On very slow or metered links, --layers 20:42 is also supported: the coordinator will load the output head and compute logits locally, trading extra coordinator work for smaller per-token replies.
The table below shows the same two M5 Max hosts, the same 91 GB Flash quant, coordinator --layers 0:19, worker --layers 20:output, an 8192-token prompt from speed-bench/promessi_sposi.txt, and 128 generated tokens. WiFi and Internet numbers vary with local conditions, but the shape is the important part: high latency hurts generation directly, while lower bandwidth also pulls down long-prefill speed.
| Link | Addresses | Ping avg | Prefill | Generation |
|---|---|---|---|---|
| Thunderbolt 5 | 169.254.43.68 -> 169.254.12.245 |
0.45 ms | 582.99 t/s | 25.09 t/s |
| WiFi | 192.168.1.57 -> 192.168.1.95 |
77.20 ms | 250.70 t/s | 10.70 t/s |
| Internet / VPN | 10.77.0.4 -> 10.77.0.3 |
152.10 ms | 114.88 t/s | 3.63 t/s |
The Internet/VPN case is not meant to be a good interactive experience. It is still useful for collective testing: multiple people can temporarily combine machines to run a larger model that would not fit on any single host, accepting slow decode in exchange for being able to inspect the model at all.
Use the coordinator exactly like normal ./ds4: interactive chat, /read, and ordinary generation go through the same high-level session API. The same distributed options are also wired into ds4-agent, ds4-eval, and ds4-bench. For benchmarks, workers should already be running; ds4-bench waits until a complete route is available.
Useful tuning and diagnostics:
./ds4-bench \
-m gguf/DeepSeek-V4-Flash-Q4KExperts-F16HC-F16Compressor-F16Indexer-Q8Attn-Q8Shared-Q8Out-chat-v2.gguf \
--prompt-file speed-bench/promessi_sposi.txt \
--ctx-start 32768 \
--ctx-max 65536 \
--step-incr 32768 \
--gen-tokens 0 \
--role coordinator \
--layers 0:19 \
--listen 169.254.43.68 1234 \
--debug
--debug on the coordinator prints route formation and per-hop telemetry: layer range, token span, local evaluation time, downstream wait time, socket send time, and input/output byte counts. This is the current profiling tool for deciding whether a split is balanced. --dist-prefill-window N controls how many prefill chunks may be in flight end-to-end; the default is conservative and bounded. --dist-prefill-chunk N exists for experiments, but the default 4096-token chunk is the canonical setting and should be used unless you are explicitly validating a different chunk size.
By default DwarfStar sends hidden-state activations as 32-bit floats. To reduce traffic, pass --dist-activation-bits 16 or --dist-activation-bits 8 on the coordinator. This changes only the transport format between machines, not the model weights or KV cache. 16-bit transport halves activation traffic and is the first option to try on Ethernet or WiFi. 8-bit transport is more aggressive and should be treated as an approximate/experimental mode unless you have validated the output for your use case. However experimentally reduction activation size didn't provide a significant improvement, so this option may be removed in the future.
If a worker disconnects, the coordinator removes that worker from the active route. The request already in flight can fail, and later calls report an incomplete route until a compatible worker reconnects and sends a new registration. For live sessions, the coordinator keeps the token history and can rebuild worker KV state by replaying the prefix when the route is available again. Workers also validate a rolling 64-bit token-prefix hash on every work item, so a restarted worker at position 0 cannot silently accept work for position N; it reports the mismatch and the coordinator replays the current transcript. Ctrl+C in the CLI and agent is cooperative: DwarfStar waits for the current distributed token or prefill chunk to drain before returning control, which avoids coordinator-caused KV splits. Saved agent/server sessions use the same KV file format as single-machine sessions: during save the coordinator fetches worker-owned layer tensors and serializes one normal payload; during load it splits that payload over the currently registered route.
Distributed protocol overview
At the protocol level there are two kinds of connections. Workers keep a control TCP connection open to the coordinator and send a HELLO with their model ID, model family, quant profile, layer slice, context capacity, and data port. The coordinator uses these registrations to build a route that covers all layers. Work then moves over low-latency TCP data connections: the coordinator computes the first slice, sends a WORK frame with session ID, token positions, rolling token-prefix hashes before and after the span, route information, and hidden-state payload, and each worker computes its slice. Middle workers can forward directly to the next worker. The final worker returns logits to the coordinator, or ACKs for non-final prefill chunks so the prefill pipeline can stay full. RESULT frames echo the request ID and the post-span hash. A worker status error is handled differently from a socket failure: KV/hash mismatch can be recovered by replaying the token history on the same route, while transport failure drops the route and waits for a replacement worker. For persistent KV, the coordinator opens worker data connections and sends snapshot save/load messages for each worker-owned layer range; the disk payload remains a single agent/server cache file. The protocol has no encryption or authentication, and is not release-stable yet; coordinator and workers should be built from the same commit and used on trusted machines and trusted networks.
Tensor Parallelism over RDMA
Tensor parallelism runs a single decode across two Macs connected with a Thunderbolt 5 cable, splitting the heavy per-layer work between the two GPUs and exchanging 16-24KB partial sums at synchronization gates inside the graph (RDMA over Thunderbolt when available, a dedicated TCP socket otherwise). Unlike the pipelined distributed mode above, both machines work on the same token at the same time , so it reduces per-token latency instead of just fitting a bigger model.
Each machine keeps one contiguous half of the routed experts resident. Dense, attention, shared-expert, embedding, and output weights remain replicated. This lets a model whose routed experts do not fit on one machine run fully resident across the pair; routed kernels never touch the peer's expert half.
Running GLM 5.2 across two 128 GB MacBooks
One-time setup per boot, on both machines:
# Let the GPU wire ~117 GB (default cap is ~75% of RAM; the resident
# expert shard needs ~97.5 GiB plus KV/scratch).
sudo sysctl iogpu.wired_limit_mb=120000
# RDMA over Thunderbolt needs an IPv4 address directly on the cabled
# member interface (the bridge IP does not count). Use the interface
# that is 'active' in ifconfig, e.g. en1 on one side and en6 on the
# other. Skip this if you are fine with the TCP fallback.
sudo ifconfig en1 inet 10.99.0.2/30 alias # machine A
sudo ifconfig en6 inet 10.99.0.1/30 alias # machine B
Check the verbs device before loading the model:
rdma_ctl status
ibv_devinfo -v
The device must be active and expose the IPv4-mapped GID for the address above, for example ::ffff:10.99.0.2. A working IP ping does not prove that RDMA is active.
Both machines need the same tree, commit, and GGUF path. Tensor parallelism is always a 50/50 split with one worker, so do not pass --layers. Start the worker first; it retries while the coordinator loads. The worker must dial the address on the Thunderbolt member interface, not the bridge address:
MODEL=gguf/GLM-5.2-UD-IQ2_XXS_RoutedIQ2XXS_blk78Q2K.gguf
# Machine B: worker.
./ds4 -m "$MODEL" --tensor-parallel --role worker \
--coordinator 10.99.0.2 9911 --transport rdma
# Machine A: coordinator.
./ds4 -m "$MODEL" --tensor-parallel --role coordinator \
--listen 10.99.0.2 9911 --transport rdma -c 8192 \
-p "Tell me something about the sea."
The active verbs device and IPv4-mapped GID are selected automatically. If that is ambiguous, add --rdma-device rdma_en6 --rdma-gid-index 1 on the worker and the matching rdma_en1 flags on the coordinator. Use --transport tcp on both sides to force TCP. Tensor parallel roles are currently exposed by the ds4 CLI, not by ds4-server or ds4-agent.
Startup takes about 9 seconds per machine: each rank pre-faults its ~100 GiB shard from SSD and pins it through a Metal residency set. DeepSeek V4 Flash works the same way with its own GGUF on both machines. DeepSeek gate vectors are 16 KB and ride as one RDMA message. GLM's 6144-wide 24 KB vectors are split into two ordered RDMA messages.
Measured on two M5 Max 128 GB MacBooks (GLM 5.2, IQ2_XXS, 188 GiB):
| two Macs, tensor parallel | one Mac, SSD streaming
---|---|---
decode | ~16.8 t/s (15.4 at 4k context) | ~4.8 t/s
prefill (4096 tokens) | ~94 t/s | ~3-5 t/s
residency | fully memory-resident | streams experts from SSD
Notes: the coordinator mirrors every prompt sync and eval to the worker, so both KV caches stay in lockstep; prompt processing splits both the routed-expert GEMMs (by expert ownership) and the attention heads (a contiguous half per machine) with one bulk partial-sum exchange per layer per stage (--tensor-parallel-token-prefill selects a slower token-by-token prefill that exactly matches the single-machine arithmetic). The split graph is deterministic, but its changed floating-point reduction order is not generally byte-identical to single-machine execution.
Tensor Parallelism across CUDA GPUs
On a single CUDA server, --cuda-tensor-parallel splits DeepSeek V4 Flash tensor and routed-expert work across an even number of GPUs. This is separate from the Mac-to-Mac mode above: it does not use --role, RDMA, or the distributed layer pipeline. GPU placement and memory budgets are selected with the normal --gpu-devices and --gpu-vram options.
The device order is significant. With N devices, the first N/2 logical tiers are contiguous layer-pipeline homes and the second N/2 tiers are their tensor-parallel partners. Specify all homes first and then all partners, with the closest P2P pair at matching positions. For example, the tested L40S host uses physical pairs (0,1), (2,3), (4,5), and (6,7), expressed as 0,2,4,6,1,3,5,7. Each pair stores a 50/50 split of the routed experts, and the vocabulary head is row-sharded across the participating output tiers. Those large tensors are not duplicated. Dense attention, router, and shared expert weights are replicated within each pair.
For maximum throughput on eight 48 GB L40S cards, use the imatrix Q4 model. Its routed Q4_K layout has the native grouped multi-session kernels; the Q2 model is the lower-memory choice (including tested four-card runs), but its unsupported grouped routed shapes use the exact fallback and have lower aggregate serving throughput. Download and build the L40S target with:
./download_model.sh q4-imatrix
make cuda CUDA_ARCH=sm_89
This is the interactive-agent setup used on the eight-L40S server:
MODEL=gguf/DeepSeek-V4-Flash-Q4KExperts-F16HC-F16Compressor-F16Indexer-Q8Attn-Q8Shared-Q8Out-chat-v2-imatrix.gguf
./ds4-agent --cuda --cuda-tensor-parallel \
--gpu-vram auto \
--gpu-devices 0,2,4,6,1,3,5,7 \
--model "$MODEL" \
--ctx 100000
For serving, keep multiple KV sessions resident so decode rows can be grouped across requests. The tested host is configured for up to 16 resident sessions:
./ds4-server --cuda --cuda-tensor-parallel \
--gpu-vram auto \
--gpu-devices 0,2,4,6,1,3,5,7 \
--model "$MODEL" \
--ctx 100000 \
--batched-session 16 \
--host 0.0.0.0
The equivalent local launchers are ./run-nvidia-tp-agent.sh and ./run-nvidia-tp-server.sh. The server launcher also enables the on-disk KV cache. Reduce the session count or context size if the requested resident KV caches do not fit after model loading. CUDA TP, half-resident expert ownership, output sharding, pipelined prefill, and compatible grouped decode are selected by --cuda-tensor-parallel; no DS4_CUDA_* environment tuning is required. Without an explicit --prefill-chunk, this mode uses 2048-token chunks so the tested 16-session, 100k-context layout retains enough VRAM for resident KV caches. An explicit --prefill-chunk remains an override for other topologies.
Any even card count that can hold the selected model and graph scratch is a valid topology. On this class of 48 GB card, the useful measured endpoints are Q2 on four cards (two pipeline stages) and Q4 on eight cards (four stages). For a four-card PIX-paired subset such as physical GPUs 0,1,4,5, the ordered list is 0,4,1,5. Two cards do not have enough memory for these Flash models.
This mode currently requires DeepSeek V4 Flash and an even multi-GPU placement. GLM 5.2 instead uses normal layer placement across the selected CUDA devices. DGX Spark is a single-GPU target and must not be started with --cuda-tensor-parallel.
Reducing heat, power usage and fan noise
Long local inference runs can keep the GPU busy for extended periods. If you care more about heat, fan noise, battery life on MacBooks, or reducing thermal stress on the hardware than about maximum throughput, use --power N.
--power 100 is the default and means full speed. Lower values ask DwarfStar to target that percentage of GPU usage: --power 70 targets about 70%, --power 50 targets about half usage, and so forth. DwarfStar does this by measuring GPU work time and inserting small sleeps between work units: during prefill it sleeps between layers, and during generation it sleeps between decoded tokens. This reduces sustained load without changing model output.
The option is available on the CLI, server, agent, eval, and benchmark tools for DeepSeek models. GLM 5.2 currently accepts only --power 100. For example:
./ds4 --power 50
./ds4-agent --power 70
./ds4-server --power 40 --ctx 100000
DwarfStar features a native coding agent that works in a different way than most other systems: the inference is controlled from within the agent itself, without socket/API boundaries, so the session is represented by the on-disk KV cache itself. Moreover the tools and the system prompt are all designed vertically for DeepSeek v4 Flash and PRO. This provides a few advantages:
- Low latency experience, bounded mainly by the prefill speed limits. Displaying of generated text, tool calling, start of a new session are always instantaneous.
- Live progress bar during prefill time.
- No DSML tool calling conversion, the tools are handled natively in the LLM format.
- KV cache mismatch are impossible by construction, the current state is always the truth.
- Everything is tuned for this model.
- Ability to switch saved sessions with
/listand/switch; full KV sessions resume without a prefill stage.
Agent sessions are stored in ~/.ds4/kvcache. Use /save to persist the current session, /list to show saved sessions sorted by recent update time, and /switch <sha> to resume one of them. The session ID is stable across future saves and is derived from the first user prompt and creation time. /del <sha> removes a saved session. /strip <sha> keeps the rendered conversation text and title but removes the heavy KV payload; switching to a stripped session rebuilds the KV cache by prefilling the saved text.
Use --chdir /path/to/ds4 when launching ds4-agent from another directory, so relative runtime files such as metal/*.metal resolve from the project tree.
However while the system already works, there is a lot of work to do in order to make it ready for prime time. When finally the agent will reach the wanted shape, we will likely split the server and the client creating a stateful session-based protocol that can recreate all that in a client-server way.
ds4-bench measures instantaneous prefill and generation throughput at context frontiers instead of reporting one whole-run average. It loads the model once, walks a fixed token sequence to frontiers such as 2048, 4096, 6144, and uses incremental prefill so each row measures only the newly-added token interval. After each frontier it saves the live KV state to memory, generates a fixed greedy non-EOS probe, restores the memory snapshot, and continues prefill.
./ds4-bench \
-m ds4flash.gguf \
--prompt-file speed-bench/promessi_sposi.txt \
--ctx-start 2048 \
--ctx-max 65536 \
--step-incr 2048 \
--gen-tokens 128
The example file is a cleaned public-domain Project Gutenberg text of Alessandro Manzoni's I Promessi Sposi (ebook #45334), with the Gutenberg header and footer removed: https://www.gutenberg.org/ebooks/45334.
Use --step-incr N for different linear spacing, or --step-mul F for exponential sweeps. Output is CSV with one row per frontier: latest prefill interval tokens/sec, generation tokens/sec at that frontier, and kvcache_bytes.
Sessions prefill long prompts in 4096-token chunks by default. Use --prefill-chunk 2048, for example, to match the strict official-vector checkpoint path. Changing the chunk changes the KV checkpoint/logit path, so compare it as an explicit run configuration. Chunked Metal prefill reuses the same range-capable layer-major graph for each chunk, preserving absolute compressor/indexer boundaries while avoiding the old per-layer chunk dispatch path.
ds4-eval is a small real-model integration benchmark. It is not a leaderboard runner and should not be reported as an official GPQA, SuperGPQA, AIME, or security benchmark score: the questions are an embedded 92-item subset chosen to make local regression testing useful and visually inspectable. The program loads the real GGUF, renders DeepSeek chat prompts, streams sampled tokens in a split-screen TUI, grades the final answer, and prints a per-question report with prompt tokens, generated tokens, pass/fail state, the model answer, and the correct answer.
./ds4-eval -m ds4flash.gguf --trace /tmp/ds4-eval.txt
The default run uses --tokens 16000, thinking mode enabled, and a soft/hard </think> budget cutoff so the model has room to produce a visible answer. ds4-eval sizes the context internally from the largest selected prompt plus the generation budget, and refuses runs that would need more than 1M context tokens. Press p to pause, q to exit and print the report, Up/Down to inspect or select another question, and Enter to run the selected question next. --plain disables the TUI.
Use --regrade-trace /path/to/trace.txt to replay the current answer extractor and scorer against a prior --trace file without loading the model or regenerating tokens. This is useful when auditing evaluator changes: it shows which cases changed, the old picked answer, the new picked answer, and a pass/fail summary.
For inference changes that can affect generation drift, keep this deterministic q1..q4 token-count gate in the test plan:
./ds4-eval \
-m ds4flash.gguf \
--plain \
--questions 4 \
--tokens 2048 \
--temp 0 \
--seed 1
The generated-token counts must stay aligned with the baseline:
| Question | Expected state | Expected generated tokens | Expected given/correct |
|---|---|---|---|
| 1 | PASSED |
2048 | B / B |
| 2 | PASSED |
438 | C / C |
| 3 | PASSED |
666 | 70 / 70 |
| 4 | FAILED |
2048 | A / C |
The first 75 embedded questions are interleaved as 25 GPQA Diamond, 25 audited SuperGPQA, and 25 AIME 2025 problems. The final 17 are an audited COMPSEC subset of reduced single-function C/C++ vulnerability-localization questions. The model is asked for the single best source line, or the smallest exact line set only when the bug cannot be localized to one line; the scorer accepts small audited ranges only when adjacent lines are equivalent locations for the same bug. The order is intentionally progressive: early questions are useful smoke tests, while later questions are hard enough that a strong reasoning model should still miss some of them. The SuperGPQA slice is curated rather than blind: upstream rows with wrong keys, missing figures, or underspecified prompts are replaced with cleaner rows.
The set should be treated as a hard capability regression suite rather than a pass/fail unit test.
- GPQA Diamond contributes graduate-level science questions with multiple-choice answers. DeepSeek's model card reports strong results on full GPQA Diamond in thinking mode, but individual items still require careful physics, chemistry, or biology reasoning and are easy to lose with a small prompt/rendering or sampling regression.
- SuperGPQA contributes broad specialist knowledge and domain-transfer questions. The model-card SuperGPQA number is much lower than GPQA Diamond, so these items are expected to be uneven: some look mundane, others require niche professional knowledge or exact interpretation of a translated-style exam question.
- AIME 2025 contributes exact-answer contest math. These are often the most unforgiving items in the set: no multiple-choice prior, no partial credit, and a single arithmetic or algebraic slip changes the grade.
- COMPSEC contributes single-function C/C++ security reasoning items reduced from public CVE writeups. These are not exploit prompts: the task is to identify the best source line where the defensive code flaw is introduced, or return
0for a safe function.
In practice this means ds4-eval should not be expected to produce a perfect 92/92 run. It is meant to answer a more useful engineering question: after a kernel, quantization, prompt-rendering, KV-cache, or tool-streaming change, does DeepSeek V4 Flash still solve a representative mix of hard science, broad knowledge, exact math, and security-code problems while using the same inference path users run?
One-shot prompt:
./ds4 -p "Explain Redis streams in one paragraph."
No -p starts the interactive prompt:
The interactive CLI is a real multi-turn chat. It keeps the rendered chat transcript and the live graph KV checkpoint, so each turn extends the previous conversation. Useful commands are /help, /think, /think-max, /nothink, /ctx N, /read FILE, and /quit. Ctrl+C interrupts the current generation and returns to ds4>.
The CLI defaults to thinking mode. Use /nothink or --nothink for direct answers. --mtp MTP.gguf --mtp-draft 2 enables the optional MTP speculative path; it is useful only for greedy decoding, currently uses a confidence gate (--mtp-margin) to avoid slow partial accepts, and should be treated as an experimental slight-speedup path.
Start a local OpenAI/Anthropic-compatible server:
./ds4-server --ctx 100000 --kv-disk-dir /tmp/ds4-kv --kv-disk-space-mb 8192
Use --chdir /path/to/ds4 when launching ds4-server from another directory, so relative runtime files such as metal/*.metal resolve from the project tree.
By default the server keeps one mutable backend/KV checkpoint in memory, so stateless clients that resend a longer version of the same prompt can reuse the shared prefix instead of pre-filling from token zero.
--batched-session N preallocates N independent resident KV sessions. Ready decode steps are evaluated together, while long prefills alternate in bounded chunks so one request does not block every decoder. Requests beyond N wait for a resident slot. If disk KV caching is enabled, an idle slot is persisted before reuse and can be restored when that conversation returns; an active request is never evicted. Choose N and --ctx so all resident KV allocations fit in GPU memory. Without this option, inference retains the original single-session behavior.
Batching is exact: when a native batched kernel is unavailable, DwarfStar runs the affected rows in a fixed order and returns the same full logits as separate session evaluations. The current backend behavior is:
| Backend and model | Session execution |
|---|---|
| Metal, resident DeepSeek Flash | Native shared-expert and QKV batching from two rows upward when supported; ordered fallback otherwise. |
| Metal, GLM 5.2 | Ordered exact fallback. |
| CUDA, DeepSeek Flash on a supported multi-GPU TP/EP layout | Native decode and mixed prefill/decode, with exact fallbacks for unsupported kernel shapes. |
| CUDA single GPU, including DGX Spark | Ordered exact fallback. |
N resident sessions allocate N KV states, so a context size that fits once may not fit eight times. Native batching can improve aggregate throughput; an ordered fallback provides concurrency and fairness, but not the same speedup. MTP speculative decoding is disabled while native session batching is active.
Supported endpoints:
GET /v1/modelsGET /v1/models/deepseek-v4-flashGET /v1/models/deepseek-v4-proPOST /v1/chat/completionsPOST /v1/responsesPOST /v1/completionsPOST /v1/messages
The Flash and PRO model endpoints are compatibility aliases. They both report the model currently loaded from the GGUF passed with -m; the endpoint name does not select a different model.
/v1/chat/completions accepts the usual OpenAI-style messages, max_tokens/max_completion_tokens, temperature, top_p, top_k, min_p, seed, stream, stream_options.include_usage, tools, and tool_choice. Tool schemas are rendered into DeepSeek's DSML tool format, and generated DSML tool calls are mapped back to OpenAI tool calls.
/v1/responses accepts OpenAI Responses-style input, instructions, tools, tool_choice, max_output_tokens, temperature, top_p, stream, and reasoning. It is the preferred endpoint for Codex CLI. The server keeps Responses continuations bound to live state when possible, and can fall back to the same DSML rendering and KV prefix reuse used by chat completions.
/v1/messages is the Anthropic-compatible endpoint used by Claude Code style clients. It accepts system, messages, tools, tool_choice, max_tokens, temperature, top_p, top_k, stream, stop_sequences, and thinking controls. Tool uses are returned as Anthropic tool_use blocks.
Default sampled API generation uses temperature=1, top_p=1, and min_p=0.05, so the default filter is relative probability rather than nucleus mass. In thinking mode DwarfStar applies those fixed sampling defaults to any knob the request omits, matching DeepSeek's fixed-thinking API behavior, but sampling parameters set explicitly in the request always win: a temperature=0 request is greedy through the whole reasoning phase, so benchmark harnesses get deterministic thinking-mode output.
The chat, Responses, and Anthropic endpoints support SSE streaming. In thinking mode, reasoning is streamed in the native API shape instead of being mixed into final text. OpenAI chat streaming also streams tool calls as soon as the DSML invocation is recognized: the tool header is sent first, then parameter bytes are forwarded as tool_calls[].function.arguments deltas while generation continues. The Anthropic endpoint streams thinking and text live, then emits structured tool_use blocks when the generated tool block is complete. The Responses endpoint streams the Responses event lifecycle expected by Codex, including response.output_text.delta, function-call argument events, and terminal response.completed / response.incomplete / response.failed events.
For browser JavaScript clients served from another origin, start the server with --cors to emit Access-Control-Allow-* headers. This only changes HTTP headers; it does not expose the server on the LAN. Use --host 0.0.0.0 explicitly when remote machines should be able to connect.
Tool call handling and canonicalization
DeepSeek V4 emits tool calls as DSML text. Agent clients do not send that same text back on the next request: they send normalized OpenAI/Anthropic JSON tool-call objects. If the server re-rendered those objects slightly differently, the rendered byte prefix would no longer match the live KV checkpoint and the next turn would have to be rebuilt.
The first line of defense is exact replay. Every tool call gets an unguessable API tool ID, and the server remembers tool id -> exact sampled DSML block in a bounded in-memory map backed by radix trees. When the client later sends that tool ID back, the prompt renderer uses the exact DSML bytes the model sampled, not a freshly formatted approximation. This map can also be saved inside KV cache files, so exact replay survives server restarts for cached histories.
Canonicalization is only the backup path. If the exact DSML block is missing, or exact replay is disabled with --disable-exact-dsml-tool-replay, the server renders a deterministic DSML form from the JSON tool object. After a tool-call turn, it compares the live sampled token stream with the prompt that the next client request will render. If needed, it rewrites the live checkpoint, or falls back to an older disk KV snapshot and replays only the suffix. This keeps the model continuation aligned with the stateless API transcript.
During generation, the server also treats DSML syntax differently from payload. When the model is emitting stable protocol structure such as DSML tags, parameter headers, JSON punctuation, or closing markers, sampling is forced to temperature=0 so the tool call stays parseable. This greedy mode does not apply to argument payloads: string=true parameter bodies and JSON string values, including file contents and edit text, use the request's normal sampling settings. That separation is important: deterministic decoding is helpful for syntax, but can create repeated text when applied to long code or file bodies.
Minimal OpenAI example:
curl http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model":"deepseek-v4-flash",
"messages":[{"role":"user","content":"List three Redis design principles."}],
"stream":true
}'
ds4-server can be used by local coding agents that speak OpenAI-compatible chat completions. Start the server first, and set the client context limit no higher than the --ctx value you started the server with:
./ds4-server --ctx 100000 --kv-disk-dir /tmp/ds4-kv --kv-disk-space-mb 8192
You can use larger context and larger cache if you wish. Full context of 1M tokens is going to use more or less 26GB of memory (compressed indexer alone will be like 22GB), so configure a context which makes sense in your system. With 128GB of RAM you would run the 2-bit quants, which are already 81GB, 26GB are going to be likely too much, so a context window of 100~300k tokens is wiser. However users reported being able to run 2bit quants with 250k ctx window in a Macs with just 96GB of system memory: make sure to kill processes that use too much memory, if you plan doing so ;)
The 384000 output limit below avoids token caps since the model is able to generate very long replies otherwise (up to 384k tokens). The server still stops when the configured context window is full.
For opencode , add a provider and agent entry to ~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ds4": {
"name": "ds4.c (local)",
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://127.0.0.1:8000/v1",
"apiKey": "dsv4-local"
},
"models": {
"deepseek-v4-flash": {
"name": "DeepSeek V4 Flash (ds4.c local)",
"limit": {
"context": 100000,
"output": 384000
}
}
}
}
},
"agent": {
"ds4": {
"description": "DeepSeek V4 Flash served by local ds4-server",
"model": "ds4/deepseek-v4-flash",
"temperature": 0
}
}
}
For Pi , add a provider to ~/.pi/agent/models.json:
{
"providers": {
"ds4": {
"name": "ds4.c local",
"baseUrl": "http://127.0.0.1:8000/v1",
"api": "openai-completions",
"apiKey": "dsv4-local",
"compat": {
"supportsStore": false,
"supportsDeveloperRole": false,
"supportsReasoningEffort": true,
"supportsUsageInStreaming": true,
"maxTokensField": "max_tokens",
"supportsStrictMode": false,
"thinkingFormat": "deepseek",
"requiresReasoningContentOnAssistantMessages": true
},
"models": [
{
"id": "deepseek-v4-flash",
"name": "DeepSeek V4 Flash (ds4.c local)",
"reasoning": true,
"thinkingLevelMap": {
"off": null,
"minimal": "low",
"low": "low",
"medium": "medium",
"high": "high",
"xhigh": "xhigh"
},
"input": ["text"],
"contextWindow": 100000,
"maxTokens": 384000,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
}
}
]
}
}
}
Optionally make it the default Pi model in ~/.pi/agent/settings.json:
{
"defaultProvider": "ds4",
"defaultModel": "deepseek-v4-flash"
}
For Codex CLI , use the Responses wire API:
[model_providers.ds4]
name = "DS4"
base_url = "http://127.0.0.1:8000/v1"
wire_api = "responses"
stream_idle_timeout_ms = 1000000
Then run:
codex --model deepseek-v4-flash -c model_provider=ds4
For Claude Code , use the Anthropic-compatible endpoint. A wrapper like this matches the local ~/bin/claude-ds4 setup:
#!/bin/sh
unset ANTHROPIC_API_KEY
export ANTHROPIC_BASE_URL="http://127.0.0.1:8000"
export ANTHROPIC_AUTH_TOKEN="dsv4-local"
export ANTHROPIC_MODEL="deepseek-v4-flash"
export ANTHROPIC_CUSTOM_MODEL_OPTION="deepseek-v4-flash"
export ANTHROPIC_CUSTOM_MODEL_OPTION_NAME="DeepSeek V4 Flash local ds4"
export ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION="ds4.c local GGUF"
export ANTHROPIC_DEFAULT_SONNET_MODEL="deepseek-v4-flash"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="deepseek-v4-flash"
export ANTHROPIC_DEFAULT_OPUS_MODEL="deepseek-v4-flash"
export CLAUDE_CODE_SUBAGENT_MODEL="deepseek-v4-flash"
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
export CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK=1
export CLAUDE_STREAM_IDLE_TIMEOUT_MS=600000
exec "$HOME/.local/bin/claude" "$@"
Claude Code may send a large initial prompt, often around 25k tokens, before it starts doing useful work. Keep --kv-disk-dir enabled: after the first expensive prefill, the disk KV cache lets later continuations or restarted sessions reuse the saved prefix instead of processing the whole prompt again.
DeepSeek V4 Flash has distinct non-thinking, thinking, and Think Max modes. The server defaults to thinking mode. reasoning_effort=max requests Think Max, but it is only applied when the context size is large enough for the model card recommendation; smaller contexts fall back to normal thinking. OpenAI reasoning_effort=xhigh still maps to normal thinking, not Think Max.
For direct replies, use thinking: {"type":"disabled"}, think:false, or a non-thinking model alias such as deepseek-chat.
Chat/completion APIs are stateless: agent clients usually resend the whole conversation every request. ds4-server first tries the cheap exact token-prefix check, then falls back to comparing rendered prompt bytes with decoded checkpoint bytes. The live in-memory checkpoint covers the current session; the disk KV cache makes useful prefixes survive session switches and server restarts.
For RAM reasons there is currently only one live KV cache in memory. When a new unrelated session replaces it, the old checkpoint can only be resumed without re-processing if it was written to the disk KV cache. In other words, memory cache handles the active session; disk cache is the resume mechanism for different sessions.
Enable it with:
./ds4-server --kv-disk-dir /tmp/ds4-kv --kv-disk-space-mb 8192
The cache key is the SHA1 of the rendered byte prefix, and files are named <sha1>.kv. The DS4 payload still stores the exact token IDs and graph state for that prefix. This matters for continued chats: the model may have generated one token whose decoded text is later sent back by a client as two canonical prompt tokens. A rendered byte-prefix hit can still reuse the checkpoint and tokenize only the new suffix. The file is intentionally written with ordinary read/write I/O, not mmap, so restoring cache entries does not add more VM mappings to a process that already maps the model.
Tool calls also keep a bounded exact-DSML replay map keyed by unguessable tool IDs, so client JSON history can be rendered back to the exact sampled text. The RAM map keeps up to 100000 IDs by default; tune it with --tool-memory-max-ids. Use --disable-exact-dsml-tool-replay to disable this and fall back to canonical JSON-to-DSML rendering.
On disk, a cache file is:
KVC fixed header, 48 bytes
u32 rendered_text_bytes
rendered_text_bytes of UTF-8-ish token text
DS4 session payload, payload_bytes from the KVC header
optional tool-id map section
The fixed header is little-endian:
0 u8[3] magic = "KVC"
3 u8 version = 1
4 u8 routed expert quant bits, currently 2 or 4
5 u8 save reason: 0 unknown, 1 cold, 2 continued, 3 evict, 4 shutdown
6 u8 extension flags, bit 0 = appended tool-id map
7 u8 reserved
8 u32 cached token count
12 u32 hit count
16 u32 context size the snapshot was written for
20 u8[4] reserved
24 u64 creation Unix time
32 u64 last-used Unix time
40 u64 DS4 session payload byte count
The rendered text is the tokenizer-decoded text for the cached token prefix. It is both the human-inspectable prefix and the lookup identity: its SHA1 is the filename, and a file is reusable only when those bytes are a prefix of the incoming rendered prompt. After load, the exact checkpoint tokens from the DS4 payload remain authoritative, and only the incoming text suffix after the cached bytes is tokenized.
The optional tool-id map is present only when header extension bit 0 is set. Appended sections use fixed bit order, so future extension bits can add fields without ambiguity. The map stores unguessable API tool call IDs back to the exact DSML block the model sampled. Only mappings whose DSML block is present in the rendered cached text are stored. This lets restarted servers render later client history byte-for-byte like the original model output, even if the client reorders JSON arguments.
The current tool-id map section is:
0 u8[3] magic = "KTM"
3 u8 version = 1
4 u32 entry count
For each entry:
0 u32 tool id byte length
4 u32 sampled DSML byte length
8 bytes tool id
... bytes exact sampled DSML block
The section is auxiliary replay memory, not model state. A cache hit restores the session payload first, then loads the map if present. Before rendering a request, the server can also scan cache files for the tool IDs present in the client history and load just those mappings, so an exact DSML replay can survive server restarts even when the matching KV snapshot is not the one ultimately used for the rendered-prefix hit.
The DS4 session payload starts with thirteen little-endian u32 fields:
0 magic = "DSV4"
1 payload version = 2
2 saved context size
3 prefill chunk size
4 raw KV ring capacity
5 raw sliding-window length
6 compressed KV capacity
7 checkpoint token count
8 layer count
9 raw/head KV dimension
10 indexer head dimension
11 vocabulary size
12 live raw rows serialized below
Then it stores:
u32[token_count]checkpoint token IDs.float32[vocab_size]logits for the next token after that checkpoint.u32[layer_count]compressed attention row counts.u32[layer_count]ratio-4 indexer row counts.- For every layer: the live raw sliding-window KV rows, written in logical position order rather than physical ring order.
- For compressed layers: live compressed KV rows and compressor frontier tensors.
- For ratio-4 compressed layers: live indexer compressed rows and indexer frontier tensors.
The logits are raw IEEE-754 float32 values from the host ds4_session buffer. They are saved immediately after the checkpoint tokens so a loaded snapshot can sample or continue from the exact next-token distribution without running one extra decode step. MTP draft logits/state are not persisted; after loading a disk checkpoint the draft state is invalidated and rebuilt by normal generation.
Distributed coordinator sessions use the same DSV4 payload. Worker-owned layer tensors are pulled during save and merged into the normal layer-ordered tensor stream; during load the coordinator splits that stream into the current route and pushes the relevant layer tensors back to the workers. The saved file does not retain the distributed topology.
The tensor payload is DS4-specific KV/session state, not a generic inference graph dump. It is expected to be portable only across compatible ds4.c builds for this model layout.
The cache stores checkpoints at four moments:
cold: after a long first prompt reaches a stable prefix, before generation.continued: when prefill or generation reaches the next absolute aligned frontier.evict: before an unrelated request replaces the live in-memory session.shutdown: when the server exits cleanly.
Cold saves intentionally trim a small token suffix and align down to a prefill chunk boundary. This avoids common BPE boundary retokenization misses when a future request appends text to the same prompt. The defaults are conservative: store prefixes of at least 512 tokens, cold-save prompts up to 30000 tokens, trim 32 tail tokens, and align to 2048-token chunks. The important knobs are:
Continued saves use the same alignment and are written only when the live graph naturally reaches an absolute frontier. With the defaults this means roughly every 10k tokens, independent of where the first cold checkpoint landed, so long generations leave restart points behind without persisting the fragile final few tokens.
--kv-cache-min-tokens--kv-cache-cold-max-tokens--kv-cache-continued-interval-tokens--kv-cache-boundary-trim-tokens--kv-cache-boundary-align-tokens--tool-memory-max-ids--disable-exact-dsml-tool-replay
By default, checkpoints may be reused across the 2-bit and 4-bit routed-expert variants if the rendered prefix matches. Use --kv-cache-reject-different-quant when you want strict same-quant reuse only.
The cache directory is disposable. If behavior looks suspicious, stop the server and remove it. You can investigate what is cached with hexdump as the kv cache files include the verbatim prompt cached.
The default graph backend is Metal on macOS and CUDA in CUDA builds:
./ds4 -p "Hello" --metal
./ds4 -p "Hello" --cuda
On Linux, plain make prints the available build targets instead of selecting a CUDA target implicitly. Use make cuda-spark for DGX Spark / GB10. It omits an explicit nvcc -arch because that is currently the fastest path on GB10. Use make cuda-generic for a normal local CUDA build, or set CUDA_ARCH explicitly when cross-building or when you need a known target:
make cuda CUDA_ARCH=sm_120
make cuda CUDA_ARCH=native
CUDA builds accept --gpu-vram N[,N,...] and --gpu-devices N[,N,...] in the CLI, server, agent, and benchmark. VRAM values are per-device GiB budgets; --gpu-vram auto uses the free memory reported by CUDA. The device list controls the placement order and must have the same number of entries as an explicit budget list. Placement reserves graph and KV memory for the requested context and refuses to start if model layers would spill to the CPU.
Without --cuda-tensor-parallel, CUDA uses normal layer placement across the listed devices. This is also the supported multi-GPU layout for GLM 5.2:
./ds4 -m gguf/GLM-5.2-UD-Q2_K_RoutedQ2K.gguf \
--gpu-vram auto --gpu-devices 0,2,4,6,1,3,5,7 \
--ctx 32768 -p "Hello"
For the DeepSeek Flash tensor/expert-parallel layout, see "Tensor Parallelism across CUDA GPUs" above.
There is also a CPU reference/debug path:
./ds4 -p "Hello" --cpu
make cpu
./ds4
./ds4 -p "Hello"
Do not treat the CPU path as the production target. The CLI and ds4-server support the CPU backend for reference/debug use and share the same KV session and snapshot format as Metal and CUDA, but normal inference should use Metal or CUDA.
This project supports steering with single-vector activation directions; see the dir-steering directory for more information. This follows the core idea of the Refusal in Language Models Is Mediated by a Single Direction paper. You can use it to make the model more or less verbose, less likely to answer programming questions if it is a chatbot for your car rental web site, and so forth, much faster than fine-tuning. This is also useful for cybersecurity researchers who want to reduce a model's willingness to provide dual-use or offensive security guidance.
tests/test-vectors contains short and long-context continuation vectors captured from the official DeepSeek V4 Flash API. The requests use deepseek-v4-flash, greedy decoding, thinking disabled, and the maximum top_logprobs slice exposed by the API. Local vectors are generated with ./ds4 --dump-logprobs and compared by token bytes, so tokenizer/template or attention regressions show up before they become long generation failures. The C runner pins a 2048-token prefill chunk for this strict API-vector comparison.
The core local tests are driven by the C runner, with a small ds4-eval extractor self-test run first:
make test # ./ds4-eval --self-test-extractors && ./ds4_test --all
./ds4_test --logprob-vectors
./ds4_test --server
The batching tests are model-backed and must run on the matching GPU backend:
# Metal, with DS4_TEST_SESSION_COUNT set to 2, 4, 8, and 16.
DS4_TEST_MODEL=/path/to/model.gguf DS4_TEST_SESSION_COUNT=4 \
make test-metal-session-batch
# CUDA multi-GPU Flash.
DS4_TEST_MODEL=/path/to/model.gguf make test-cuda-session-batch
DS4_TEST_MODEL=/path/to/model.gguf make test-cuda-mixed-batch
For GLM, run the same Metal session test with a GLM GGUF and run tests/glm_long_context_smoke.sh /path/to/model.gguf. The official 100-case quality scorers, two-Mac TCP/RDMA tests, CUDA matrix, and manual agent checks are release gates rather than quick local tests; follow QA_BEFORE_RELEASES.md.
When a generation looks wrong, three small tools are usually enough to get a first answer:
./ds4 --dump-tokens -p "..."
./ds4 --dump-logprobs /tmp/out.json --logprobs-top-k 20 --temp 0 -p "..."
./ds4 --dump-logits /tmp/logits.json --metal --nothink --prompt-file prompt.txt
./ds4-server --trace /tmp/ds4-trace.txt ...
--dump-tokenstokenizes the-por--prompt-filestring exactly as written, recognizes DS4 protocol specials, and then exits before inference starts. For example, the DSML tool close marker starts as two tokens:</and|DSML|.--dump-logprobsstores a greedy continuation with the top local alternatives at each step, which helps separate sampling choices from logit/model issues.ds4-server --tracewrites the rendered prompts, cache decisions, generated text, and tool-parser events for a whole agent session.
The DwarfStar logo was designed by hand by Salvatore Sanfilippo, made more graphical with AI, and manually reworked by Ben Gnomino, whose human touch made it rock.