首页
/ LocalAI ds4 后端实战:把 antirez/ds4 引擎封装成 C++ gRPC 推理服务的完整技术解析

LocalAI ds4 后端实战:把 antirez/ds4 引擎封装成 C++ gRPC 推理服务的完整技术解析

2026-09-05 20:42:54作者:卓炯娓

本文以 LocalAI 仓库中 ds4 后端的开发指南(.agents/ds4-backend.md)为主线,系统讲解 LocalAI 如何为 antirez/ds4(DeepSeek V4 Flash 单模型推理引擎)构建一套独立的 C++ gRPC 后端:从上游版本 pin 与依赖更新机制、gRPC RPC 接线形态、DSML 工具调用文本标记的流式解析、thinking 模式与磁盘 KV cache,到 SSD streaming 跑超大 MoE 模型、CUDA fat binary 构建陷阱与验证方法、硬件门控 e2e 测试,以及分层分布式推理(coordinator/worker)与模型导入器(DS4Importer)的接入细节。读完后你能掌握在 LocalAI 中构建、配置、验证并扩展 ds4 后端的完整技术路径。

后端总览:为什么是"全新 C++ 服务"而不是复用 llama-cpp

ds4 是 antirez 为 DeepSeek V4 Flash 编写的单模型推理引擎。LocalAI 对这个引擎的接入方式是:用一套全新的 C++ gRPC 服务包裹其 C API(ds4/ds4.h),代码位于 backend/cpp/ds4/。这里刻意没有 fork llama-cpp 的 grpc-server.cpp——因为 ds4 的会话模型、MoE 专家流式加载和分布式传输都是 llama.cpp 不具备的独立能力。

后端目录的实际组成(见 backend/cpp/ds4/):

版本 Pin:上游 commit 如何被锁定与更新

backend/cpp/ds4/Makefile 顶部用一行 DS4_VERSION?=<sha> 固定上游 commit(当前为 8db89fe083ae4d17c9a2428ccd29803d3ae8f577,见 Makefile):

DS4_VERSION?=8db89fe083ae4d17c9a2428ccd29803d3ae8f577
DS4_REPO?=https://github.com/antirez/ds4

Makefile 中的 ds4 目标按该 commit 做 git fetch --depth 1 浅克隆(见 Makefile),这与仓库内 llama-cpp / ik-llama-cpp / turboquant 后端的 pin 约定完全一致。CI 侧的 bump-deps 机器人(.github/workflows/bump_deps.yaml)通过 grep 这一行 DS4_VERSION?= 找到 pin,并每日自动开 PR 更新。

手动升级上游的流程是:

  1. 编辑 Makefile 中的 DS4_VERSION?= 行;
  2. 执行 make purge && make 重新拉取并构建(purge 会删掉已克隆的 ds4/ 目录),或直接依赖 CI 的 clean build。

RPC 接线形态:每条 gRPC 调用对应哪个 ds4 API

grpc-server.cpp#include "ds4.h" 与全局单例状态(g_engine / g_session,ds4 按设计是单引擎/单进程)可以看到各 RPC 的实现映射:

RPC 实现
Health, Free, Status 轻量;Health 不依赖引擎
LoadModel ds4_engine_open + ds4_session_create;后端在编译期确定:DS4_NO_GPU → CPU,__APPLE__ → Metal,否则 CUDA
TokenizeString ds4_tokenize_text
Predict ds4_engine_generate_argmax + DsmlParser → 一条 ChatDelta,含 content / reasoning_content / tool_calls[]
PredictStream 同上,按 token 逐条写 ChatDelta

一个值得注意的实现细节:grpc-server.cppg_loaded_model_identity 记录本进程加载的模型身份,并与请求里的 PredictOptions.ModelIdentity 比对,来自过期分布式路由的请求会被拒绝而不是用错误的模型作答;取消传播则通过 ds4_session_set_cancel 桥接 gRPC 的 ServerContext::IsCancelled()

DSML:把"字面文本标记"变成结构化 tool_calls

ds4 输出工具调用时用的不是特殊 token,而是字面文本标记(如 <|DSML|tool_calls> 等)。LocalAI 为此实现了 dsml_parser.{h,cpp} 流式状态机,把 token 字节流分类为 CONTENT / REASONING / TOOL_START / TOOL_ARGS / TOOL_END 事件(见 dsml_parser.hParserEvent)。其内部状态机为 TEXT / THINK / TOOL_CALLS / INVOKE / PARAM_VALUEdsml_parser.h),并处理了一个流式解析的难点:chunk 尾部可能只是"疑似标记前缀",会被缓冲进 buf_ 等待下一个 chunk 消歧(TryConsumeMarker / DrainPlain)。

IsInDsmlStructural() 是另一个关键能力:当解析器处于 DSML 结构位置(TOOL_CALLS / INVOKE,即协议字节应逐字输出的区域)时返回 true,调用方应在下一次采样前据此强制贪心解码(temperature=0);普通正文与参数值位置则沿用用户的采样设置。这条规则镜像了上游 ds4-server 中 dsml_decode_state_uses_payload_sampling 的行为。

反向方向由 dsml_renderer.{h,cpp} 负责:把 OpenAI 格式的 tool_callsrole=tool 消息重新渲染成 DSML,供下一轮对话拼接进 prompt。

Thinking 模式:两个 Metadata 开关

PredictPredictOptions.Metadata 控制思考行为:

  • "enable_thinking":开/关 thinking(默认开);
  • "reasoning_effort":值为 "max""xhigh" 时选择 DS4_THINK_MAX,其余任何值映射为 DS4_THINK_HIGH

选定模式后通过 ds4_chat_append_assistant_prefix 传给引擎。解析侧则依赖 DsmlParserREASONING 事件把思考内容分离到 reasoning_content,最终输出到 content 的都是思考块闭合之后的正文——这也是后文 CUDA 架构缺陷(思考块永不闭合导致 content 为空)能被 e2e 测试捕获的原因。

磁盘 KV cache:SHA1 键控的文件缓存

kv_cache.{h,cpp} 实现了一个 SHA1 键控的文件缓存,基于 ds4 公开的 ds4_session_save_payload / ds4_session_load_payload API 序列化会话 KV 状态。每请求开启方式:

options:
  - kv_cache_dir:/some/path

需要注意两个事实边界:其文件格式是 LocalAI 自有的,与 ds4-server 的 KVC 文件位兼容(互通是后续计划);另外从 grpc-server.cpp 可见 g_kv_cache_dir 为空时磁盘缓存整体禁用,即完全按需启用。

引擎选项:声明式表驱动的 LoadModel 选项映射

这是 ds4 后端最具工程特色的部分。ds4_engine_options无反射的纯 C 结构体,因此字段集必须在某一处枚举一次。实现选择了声明式表kEngineOptSpecs[] + apply_engine_optiongrpc-server.cpp)。未来新增引擎旋钮 = 表里加一行,而不是新分支。

表结构按类型区分(Bool / Int / Uint / Float / Str / Gib / CacheExperts),关键语义:

  • 未知 key 直接忽略(向后兼容:调用方可能传入混合选项集);
  • 裸 flag(如不带冒号的 ssd_streaming)按 true 处理,所以模型 YAML 可以写 options: ["ssd_streaming"](见 parse_bool_optiongrpc-server.cpp);
  • 路径型值mtp_pathexpert_profile_pathdirectional_steering_file)相对路径会相对模型目录解析——gallery 条目因此可以用裸文件名引用其伴生下载文件;绝对路径直接透传;
  • 两个字段需要 ds4 自己的类型化解析器:simulate_used_memoryNGBds4_parse_gib_arg)和 ssd_streaming_cache_experts(专家数或 NGB,走 ds4_parse_streaming_cache_experts_arg,一次写入 experts+bytes 两个字段);
  • 字符串选项拷入 storage 向量,且必须预留容量,避免 push_back 重分配导致 c_str() 悬空(源码注释明确警告,grpc-server.cpp)。

当前接线的 key(与 kEngineOptSpecs 一一对应):

mtp_pathmtp_draftmtp_marginprefill_chunkpower_percentwarm_weightsqualityssd_streamingssd_streaming_coldssd_streaming_preload_expertsssd_streaming_cache_experts(数量或 NGB)、simulate_used_memoryNGB)、expert_profile_pathdirectional_steering_filedirectional_steering_attndirectional_steering_ffn

ds4_role / ds4_layers / ds4_listen / ds4_route_timeout / kv_cache_dir专属处理(校验 + coordinator 接线),不进入表中——例如 ds4_layersSTART:END / START:output 解析由 grpc-server.cppparse_layers_spec 完成,output 后缀表示"直到最后一层(含 head)"。

SSD streaming:跑下内存放不下的模型

ds4 的 SSD streaming 让非路由权重保持常驻,在 cache miss 时从 GGUF 流式读取被路由到的 MoE 专家,把"放得下/放不下"变成一个速度光谱。该能力仅限 Metal(Darwin)——在 CUDA/CPU 上是 no-op。

启用与调参:

options:
  - ssd_streaming
  # 可选:显式设定路由专家缓存(专家数或 NGB 预算;省略则用 ds4 自动的
  # "工作集 80%" 预算)
  - ssd_streaming_cache_experts:16GB

基于此构建的 gallery 条目(见 gallery/index.yaml):

  • deepseek-v4-flash-q4-ssd:153 GB 的 Flash 模型跑在 128 GB 内存的 Mac 上;
  • deepseek-v4-pro-q2-ssd:433 GB 的 Pro 模型(实验性)。

Makefile 可以看到 ds4_ssd.o / ds4_layer_pack.o / ds4_distributed.o / ds4_tp.o 被描述为"与 GPU 无关的翻译单元",每种 GPU 模式都无条件链接——这与"SSD streaming 仅限 Metal"形成对照,说明这些对象同时服务于分布式传输与层放置等共享逻辑。

CUDA 架构:fat binary 与"JIT PTX 静默损坏"的教训

这是开发指南中最有实战价值的一节。

问题根源

Makefile 直接驱动上游的对象目标$(MAKE) -C ds4 ds4.o ds4_cuda.o ...),从而绕开了上游的自我保护:上游的 cuda 目标在没有设置 CUDA_ARCH拒绝构建,只提供 cuda-spark(sm_121,DGX Spark / GB10)与 cuda-generic(native)两种选择。直接编译对象文件时,nvcc 在没有任何 -arch 的情况下工作,内核以默认架构的 JIT PTX 运行。

在 GB10(sm_121)上,这导致静默的推理损坏:超过 128 token 的 prefill batch 输出与 prompt 无关的文本,且思考块永不闭合,content 因此为空;同时 prefill 吞吐掉近两个数量级(同机同模型 4.21 t/s vs 325.70 t/s,约 77 倍差距)。短 prompt 保持正确,所以该缺陷长期未被发现。

Makefile 的解决方案

MakefileCUDA_MAJOR_VERSION(构建矩阵已声明的构建参数,由 Dockerfile.ds4 转发)与 uname -m 选择 gencode 列表,并以 NVCC_ARCH_FLAGS 传给子 make:

  • CUDA_MAJOR_VERSION=13 + amd64:sm_80 / 86 / 89 / 90a / 100a / 103a / 120a / 121a;
  • CUDA_MAJOR_VERSION=13 + aarch64(l4t):sm_87(Orin)/ 90a / 100a / 110(Thor)/ 121a(GB10);
  • 架构列表直接抄自 backend/go/vllm-cpp/Makefile,保证两个 CUDA 镜像覆盖相同 GPU 集合;
  • 之所以用 NVCC_ARCH_FLAGS 而不是 CUDA_ARCH:上游的 CUDA_ARCH 只接受单一值-arch=$(CUDA_ARCH)),无法表达发行镜像需要的 fat binary;而命令行赋值可以赢过上游 Makefile 里的 :=

两个兜底策略:

  • CUDA_MAJOR_VERSION 为空 → 视为本地开发者构建,回退到上游的 native(需要本机有 GPU);
  • 无法识别的值 → 硬错误($(error ...))。因为 CI runner 没有 GPU,静默 native 在那里正是这段逻辑要防住的失败模式。

为什么刻意不定义 DS4_CUDA_HAVE_MXF4

Makefile 明确不设置该宏:上游只为单架构 sm_120/sm_121 构建定义它,且代码用裸 #ifdef 而非 __CUDA_ARCH__ 保护,因此无法与较老架构拼进同一个 fat binary。它门控的是可选的 MXFP4 indexer 快速路径,其 #ifndef 分支返回 0 回退通用路径——省略它损失的是速度,不是正确性

验证一次构建

第一步:只解析、不编译,查看某个配置最终会落到哪些 flag:

make -C backend/cpp/ds4 BUILD_TYPE=cublas CUDA_MAJOR_VERSION=13 NATIVE=false \
  --eval='show: ; @echo [$(DS4_ARCH_MAKEVARS)]' show

注意:不要用 make -n 做这件事——该 recipe 是 +$(MAKE) ...+ 前缀表示即使在 -n 模式下也会真正执行。

第二步:直接演练失败模式。 它只在超过一个 prefill batch 后出现,普通 predict 能力测不出,需要 long_prefill

BACKEND_BINARY=$(pwd)/backend/cpp/ds4/package/run.sh \
BACKEND_TEST_MODEL_FILE=/path/to/ds4flash.gguf \
BACKEND_TEST_CAPS=health,load,predict,long_prefill \
go test -count=1 -timeout=30m -v ./tests/e2e-backends/...

构建矩阵

Build 位置 说明
cpu-ds4(amd64 + arm64) Linux GHA ds4 认为 CPU 仅具调试价值;只用于接线测试
cuda13-ds4(amd64 + arm64) Linux GHA + DGX Spark 验证 Linux 上的主要生产路径
ds4-darwin(arm64) macOS GHA runner Metal;类似 llama-cpp-darwin,使用 scripts/build/ds4-darwin.sh

cuda12 被有意排除;ROCm / Vulkan / SYCL 不适用。

硬件门控的验证(opt-in e2e)

tests/e2e-backends/backend_test.go 支持 BACKEND_BINARY 模式,用环境变量指定已构建的后端与测试模型:

BACKEND_BINARY=$(pwd)/backend/cpp/ds4/package/run.sh \
BACKEND_TEST_MODEL_FILE=/path/to/ds4flash.gguf \
BACKEND_TEST_CAPS=health,load,predict,stream,tools \
BACKEND_TEST_TOOL_PROMPT="What's the weather in Paris?" \
go test -count=1 -timeout=30m -v ./tests/e2e-backends/...

CI 本身不加载模型——整套测试套件完全通过环境变量主动开启,无硬件的 CI 上自动跳过,有硬件的开发者/DGX Spark 上可直接回归。

分布式模式:分层推理与"反转"拓扑

ds4 支持分层(layer-split)分布式推理:模型单主机放不下时按 transformer 层切分;GGUF 必须存在于每台机器,各机器只加载自己的切片。其拓扑与 llama.cpp 相反coordinator 监听,worker 主动拨入

ds4-worker 二进制

package.shgrpc-server 一起打包进 package/。它链接同样的引擎对象加上 ds4_distributed.o,但不依赖 gRPC/protobuf(走 ds4 自己的 TCP 传输),因此即使在 grpc-server 无法构建的环境也能编译。运行时执行 worker 服务循环(ds4_dist_run)。

Coordinator 接线

LoadModelModelOptions.Options(来自模型 YAML 的 options:)携带以下键时,ds4 grpc-server 扮演 coordinator(ds4_role 缺失 → 单节点,向后兼容):

  • ds4_role:coordinator:启用分布式模式;
  • ds4_layers:0:19:coordinator 自己的切片(含端点;N:output 表示含 head);
  • ds4_listen:0.0.0.0:1234:worker 拨入的地址;
  • ds4_route_timeout:60(可选,默认 60 秒):Predict/PredictStream 等待路由组建完成的超时,超时返回 gRPC UNAVAILABLE

grpc-server.cpp 可以看到对应的全局状态 g_distributedg_route_timeout_sec,以及"等待 worker 路由覆盖全部层"(ds4_session_distributed_route_ready)的阻塞逻辑。

Worker CLI

local-ai worker ds4-distributed -- <ds4-worker args> 会解析 ds4 后端并 exec 打包的 ds4-worker(参数原样透传),例如:

local-ai worker ds4-distributed -- \
  --role worker --model /models/ds4flash.gguf \
  --layers 20:output --coordinator <host> 1234

分布式 e2e(opt-in)

tests/e2e-backends/backend_test.go 中的分布式测试由 BACKEND_TEST_DS4_DISTRIBUTED=1 门控,配合 BACKEND_TEST_DS4_WORKER_BINARYBACKEND_TEST_DS4_WORKER_LAYERSBACKEND_TEST_DS4_COORDINATOR_LAYERSBACKEND_TEST_DS4_LISTEN 四个变量完整配置两侧拓扑。

模型导入器:DS4Importer 的自动检测

core/gallery/importers/ds4.go 中的 DS4Importer 通过两个信号自动检测 ds4 权重:

  1. URI 匹配 antirez/deepseek-v4-gguf 仓库名;
  2. 文件名匹配 DeepSeek-V4-Flash-*.gguf 模式。

注册顺序有讲究:它必须注册在 LlamaCPPImporter 之前defaultImporters 列表)——两者都会匹配 .gguf,但 ds4 的特征更具体,而匹配是 first-match-wins。导入器输出 backend: ds4,本地文件名统一为 ds4flash.gguf(与 ds4 自身 CLI 的默认值一致,见 ds4.go 注释),并禁用 Go 侧的自动工具解析回退——因为 C++ 后端已经通过 DsmlParser 原生输出 ChatDelta.tool_calls,无需二次解析。

此外,ds4 还出现在 core/http/endpoints/localai/backend.go 的 pref-only 后端列表中,让 /import-model UI 可以把 ds4 作为手动选项暴露给希望强制指定后端的用户(针对非规范 URI 的场景)。

关键文件速查

内容 路径
后端 gRPC 服务(含声明式选项表) backend/cpp/ds4/grpc-server.cpp
上游 pin 与 CUDA 架构选择 backend/cpp/ds4/Makefile
DSML 流式解析状态机 backend/cpp/ds4/dsml_parser.h / dsml_parser.cpp
DSML 反向渲染 backend/cpp/ds4/dsml_renderer.h / dsml_renderer.cpp
磁盘 KV cache backend/cpp/ds4/kv_cache.h / kv_cache.cpp
分布式 worker 入口 backend/cpp/ds4/worker_main.c
打包 / 运行脚本 backend/cpp/ds4/package.sh / backend/cpp/ds4/run.sh
构建镜像 backend/Dockerfile.ds4
硬件门控 e2e 套件 tests/e2e-backends/backend_test.go
模型导入器 core/gallery/importers/ds4.go
SSD streaming gallery 条目 gallery/index.yaml
依赖自动更新工作流 .github/workflows/bump_deps.yaml

小结

LocalAI 的 ds4 后端展示了一套可复制的"单引擎 C++ gRPC 后端"工程范式:用声明式表把无类型 C 结构体的选项面一次性收敛;用状态机解析器把引擎的字面文本协议还原成 OpenAI 语义(content / reasoning_content / tool_calls);用编译期架构矩阵 + opt-in 硬件 e2e(long_prefill、分布式门控)把"CI 无 GPU"环境下的静默正确性风险压到最低;再用分层 coordinator/worker 拓扑补齐单主机装不下大 MoE 模型的最后一块拼图。所有能力都可以通过模型 YAML 的 options: 逐键开启,对未升级配置完全向后兼容。

登录后查看全文
热门项目推荐
相关项目推荐