[控场AI]
· 13 分钟阅读· 6,687 字

本地部署Jev决策API:Docker与DGX Spark实测

本地部署Jev决策API:Docker与DGX Spark实测

在DGX Spark本地运行决策API:部署要点、内存口径与十一个系统的实测对比。

本文记录了一套在NVIDIA DGX Spark上本地运行Jev式决策API的完整方案,模型采用Cloudflare Clef Flash,通过本地端点提供服务。核心在于四层匹配:CPU架构、GPU驱动、发布方加载器,以及锁定的镜像digest与检查点commit。文章强调小检查点不等于小内存占用——Flash加载17.75 GiB CUDA张量,但主机可用内存峰值下降约36.06 GiB,因为共享内存架构下两套口径不可相加。在112个所有模型都接受的短用例上,托管Jev和PPLX各得112分,Flash得102分,但两分差异不足以建立通用排名。Flash展现了典型的能力边界:已提供证据24/24正确,而缺失证据对照0/8正确,说明找到证据不等于能识别其缺失。整体结论是这套本地方案适合作为可控、可重放的演练起点,而非通用的模型性能结论。

把决策模型的推理放在本地,而不是把内部项目材料发到托管服务,听起来只是换个部署方式。真正动手之后,问题会变成:选哪个检查点、用哪套推理代码、配哪个Docker镜像,以及到底需要多少内存。

下面这套方案是我最终采用的组合:在NVIDIA DGX Spark上运行Cloudflare Clef Flash,通过本地/v1/systemone端点提供服务。完整的方法、数据表和局限性记录在已发布的技术报告中,代码与启动器托管在GitHub。

Jev式决策API的接口形态

决策API的有效形态很直接:提供包含证据的state、question和明确的answer choices,响应返回选中的答案,并可以附带概率。例如给定一条重试策略和两个操作,询问哪个操作允许重试。

这里的"Jev式"描述的正是这种接口。本地检查点来自其他发布方,本教程并不会把托管的Jev模型跑在本地。托管对比使用的是typesafe-ai/jev,通过Vercel访问。

对于本地选择题,训练好的决策头直接对给定选项打分,不需要像聊天补全那样逐token生成答案。一个报告零输出token的响应,仍然可能包含一个真实的决策。

决策头(decision head) 是这里的关键机制。它与聊天补全模型的生成路径完全不同:语言模型骨干先对输入的state、question和answer choices做一次前向传播,得到一个上下文表示向量;决策头则是一个通常在骨干之上训练的小型分类或打分模块,直接对这个表示计算每个选项的分数,再经softmax得到概率分布。

因此,响应的输出token数为零并不代表没有发生推理——模型已经完成了一次完整的编码与前向计算,只是最终输出的是结构化选项而非逐字生成。这种设计带来的实际好处是延迟稳定、吞吐可预测,且答案天然对应给定的合法选项集合,不会出现生成模型偶尔跑偏到选项之外的情况。代价是每个检查点都必须由发布方提供与之匹配的决策头权重和加载逻辑,这也是后文强调必须核对发布方加载器的原因。

搭建运行时

这套方案已在NVIDIA DGX Spark(Linux ARM64)上测试通过。关键在于选择与硬件和检查点推理代码匹配的容器。换到其他机器上时,需要逐项确认四件事:

  1. 匹配CPU架构。 用docker buildx imagetools inspect IMAGE:TAG检查候选镜像的manifest,寻找你的平台标识,通常是x86 Linux工作站上的linux/amd64,或Spark上的linux/arm64。CPU模拟并不能建立GPU兼容性。
  2. 匹配GPU与驱动。 在NVIDIA的PyTorch容器发布说明和框架支持矩阵中找候选,核对其GPU架构、CUDA和驱动要求。仅CUDA主版本一致并不构成充分证据。
  3. 匹配发布方的加载器。 阅读锁定检查点的模型卡、依赖文件和运行时适配器。兼容的PyTorch容器只提供张量运行时,仍然需要正确的Transformers版本和发布方的决策头实现。
  4. 锁定你验证过的组合。 记录镜像digest、检查点commit、API commit、直接依赖版本和完整环境锁,并在该机器上测试GPU执行、加载、真实决策、溢出处理和峰值内存。

提供的启动器面向Linux ARM64。Spark上的结果不能证明其他硬件的支持情况。

拉取运行时并下载锁定检查点

测试用Spark配置有两个预构建镜像,均位于ghcr.io/krisitown/local-jev-api下:spark-20261013用于Clef Flash等九个测试检查点,spark-omni-20261013用于Clef Omni单独的兼容运行时。

要为下载和运行中的模型留出空间。公共运行时解包后约20 GB,Flash权重约18 GiB。在这个工作负载中,Flash使用了17.75 GiB的已加载CUDA张量,但造成约36.06 GiB的主机可用内存峰值下降——这是两个不同的测量口径。启动器不会替你暂停其他服务。

docker pull ghcr.io/krisitown/local-jev-api:spark-20261013

docker image inspect ghcr.io/krisitown/local-jev-api:spark-20261013 \
  --format '{{.Os}}/{{.Architecture}}'

python3 ops/download_model.py --key clef-flash

镜像检查应报告linux/arm64。下载器使用容器内安装的Hugging Face客户端,把运行时快照下载到checkpoints/clef-flash,并显式请求固定的仓库与revision。复现时应使用完整commit,而不是移动的main分支。

Dockerfile用digest锁定NVIDIA PyTorch基础镜像,其直接依赖包括transformers==5.17.0、accelerate==1.14.0、huggingface-hub==1.32.0、fastapi==0.136.3、uvicorn==0.52.4等。这些是方案测试过的直接锁定版本,并非对所有检查点都是最佳版本。Omni在单独镜像中使用Transformers 5.10.2和CPU优先加载。

在加载权重前,先验证容器能执行一个小型GPU操作:

docker run --rm --gpus all --entrypoint python3 \
  ghcr.io/krisitown/local-jev-api:spark-20261013 \
  -c 'import torch; print(torch.__version__, torch.version.cuda); assert torch.cuda.is_available(); print(torch.cuda.get_device_name(0))'

启动本地API

检查点保留在镜像之外,以只读方式挂载。Uvicorn在容器内监听0.0.0.0,而-p 127.0.0.1:18081:18081把端口发布到Spark的loopback接口。这个实验性封装没有认证层,在整个演练过程中应保持这种绑定方式。

Flash在基准测试中加载约需157秒,下载还要额外时间。等待启动完成后,检查返回的身份信息,寻找ready: true、模型Cloudflare/clef-flash、锁定的revision和backendclef。

更短的路径

手动步骤让每个环节可见。在全新环境中,启动器可以自动拉取镜像、下载锁定的检查点、启动容器、等待就绪并发送一个类型化冒烟请求:只需执行./run.sh clef-flash。它也可以接收一个已存在的完整快照。

不要在手动容器之上运行该命令:启动器会刻意拒绝替换同名容器。

测试API

模型运行后,发送一个决策请求,读取answers.q1.choice,A是符合策略的预期答案。应当检查实际响应,而不是假设HTTP请求成功就意味着答案正确。

封装还提供/inspect和/metrics,并附加_benchmark遥测。它对超大输入返回HTTP 413,而不是静默丢弃提供的state。Flash的16,384 token上限包含编码后的问题和选项,不只是state字符串。

刻意尝试另一个检查点

启动器支持全部十个测试检查点,包括Cloudflare Clef系列、Perplexity PPLX、TokenRhythm NeoHorse、Jeff和Convai Innovations Laya。每个key选择一个锁定的快照和家族专属后端。Omni会自动选择其单独镜像。所有revision都记录在检查点清单中。

停止Flash演示后,例如./run.sh jeff-2b会在默认端口启动Jeff 2B。如果需要并行运行另一个模型,可以修改主机端口。在JSON请求中使用对应的检查点ID即可。

实测结果说明了什么

技术报告覆盖全部十一个系统,包括托管Jev,并解释了测量规则。

我在单台GB10 DGX Spark上对每个本地检查点运行了336个冻结用例:288个单跳查找和48个双跳查找,包含缺失证据和近似干扰项。十个选中的完整运行产生了3,360个结果:2,065个答案和1,295个上下文拒绝。被拒绝的输入不是完成的推理,也不是错误答案。

最公平的小输入对比是所有模型都接受的112个用例:96个单跳用例,state长度为256至1,024字符,以及16个长度为1,024字符的双跳用例。

在这相同的112个短用例上,托管Jev和PPLX各得112分,NeoHorse和Clef各得104分,Flash和Omni各得102分,Jeff 2B得94分,Jeff 0.8B得77分,Laya Typed得52分,Laya得46分,Laya Multilingual得25分。

两个答案的差异不足以建立通用模型排名,技术报告中包含描述性不确定区间和完整的本地与托管对比。

小检查点不等于总内存需求

已加载分配描述的是活动模型张量,并不等于可以安全预留的内存数量。对于Flash,其过程是17.75 GiB已加载,推理期间峰值分配21.68 GiB,峰值预留29.33 GiB,主机可用内存峰值下降约36.06 GiB。预留内存包含可复用的分配器块,主机内存变化还包含运行时开销和其他活动。

Spark共享CPU和GPU内存,因此不要把CUDA和主机数字相加。在这个工作负载中,Clef和Omni各自表现出超过100 GiB的主机可用内存下降。启动和长输入同样需要空间。

已加载分配、峰值分配与峰值预留 是CUDA内存统计中三个容易混淆的口径。已加载分配指当前实际持有参数张量的显存;峰值分配是推理过程中瞬时分配的最大值,通常包含激活值和中间缓冲区;峰值预留则是PyTorch缓存分配器向CUDA申请、保留待复用的显存总量,它会高于峰值分配,因为释放的块不会立即归还驱动。

主机可用内存的下降又是另一回事:它反映的是进程整体对系统内存的影响,包含Python运行时、CUDA上下文、模型加载临时缓冲以及共享内存映射等。在DGX Spark这类CPU与GPU共享统一内存的架构上,CUDA数字和主机数字高度重叠,直接相加会严重高估需求。正确做法是看主机可用内存的实际下降幅度,并在此基础上留出启动开销和长输入的额外空间。

上下文容量与有效召回是两回事

测试的本地服务方案配置了不同的token上限。Laya为512,Laya Typed和Laya Multilingual为1,024,Jeff 0.8B和Jeff 2B为8,192,NeoHorse 4B为2,048 state加8,192分支,Flash和Clef为16,384,Omni为64,000。

这些是服务上限,不是理论骨干窗口。完整编码输入包含问题和选项。封装对溢出返回HTTP 413,不会静默截断计分请求。

Omni在边界探测中处理了63,999个编码token。在最长计分单跳层级,即131,072个state字符,它得到13/32。该探测使用重复填充文本,而计分语料具有不同的token密度——字符和token不是可互换单位。

Flash在32,768字符层级得到24/32,这值得细看:24/24的已提供针是正确的,而0/8的缺失证据对照是正确的。找到已提供的证据并不能推出能识别其缺失。

PPLX在所有224/224个被接受的用例上正确。托管Jev在这些精确的224个共同接受用例上与之持平,并处理了额外的更长用例。在整个托管运行中,Jev得到301/304接受用例正确,336个提交中有32个上下文拒绝。所有三个错误答案都涉及65,536字符的双跳查找。

速度取决于请求形态

这些是决策头,因此报告的是每秒决策数。一个四问题请求帮助了部分运行时,而其他运行时几乎不变:Flash从单问题每请求4.21决策/秒提升到四问题的9.84,但四个并发HTTP客户端并未带来同样的提升,因为我们的封装会串行化GPU推理,并发主要是在排队。它不是一个动态GPU批处理引擎。

动态批处理(dynamic batching) 与串行化的区别解释了本节的速度数据。在真正的批处理引擎中,调度器会把短时间内到达的多个请求合并成一个批次送进GPU,一次前向传播同时服务多个请求,因此并发客户端越多、单请求延迟虽可能上升,但总吞吐显著提高。

本文的封装不提供这一能力:GPU推理被串行化,后来的请求只能在队列里等待,所以四个并发HTTP客户端带来的提升远不如把四个问题打包进单个请求。后者之所以有效,是因为它们在应用层就已经是一个批次,一次前向就全部算完。理解这一点对容量规划很重要——如果工作流天然是大量独立小请求,这套方案不会靠加并发来扩展,而需要在调用侧主动做请求聚合。

我会如何使用这些结果

对于小型本地起点,Jeff 2B值得测试:112个短用例中94个正确,加载4.12 GiB,短微基准约54 ms,但双跳结果较弱。NeoHorse 4B短用例表现强,加载8.55 GiB,但state预算只有2,048 token。PPLX 27B给出了最强的本地检索结果,但内存成本大得多,服务上限为8,192 token。

Clef Flash是这里实用的演练选择:经过验证的集成路径、16,384 token预算,以及比Clef或Omni更小的占用。它在缺失证据上的失败仍然重要。Laya系列在这些运行中最轻最快,但在该英文语料上准确率较低;这项测试不能说明它们在其他语言上的表现。

实验的局限是真实的:单台Spark、单一合成英文语料、家族专属加载器和精度、没有随机化模型顺序或热控制、没有独立的重复会话。托管的GPU硬件和不可变模型revision无法获取。

把端点接入工作流

我最初想要这个接口,是为了Jev Quality Gate:取有界项目证据,提出明确的策略问题,并保留响应以便检查和重放。本地适配器分支支持可配置端点和模型,可选凭证。对于这套本地设置,不需要bearer key。

该集成证明了传输和重放能力。它的最终策略结果是不确定的,因为没有执行全局对账。它既不是通过的整体变更合规检查,也不是代码审查准确率评估。

演示结束后,在Spark上运行docker stop local-jev-flash和docker rm local-jev-flash,并恢复你暂停的任何工作负载。

仓库中包含启动器、锁定检查点清单、Dockerfile和本地基准证据。尝试一个适合你机器的检查点,然后在你项目真正需要的决策上测试它,包括那些正确证据缺失的情况。

背景补充

Digest 与 Tag 的区别 是本方案锁定可复现性的基础。Tag(如 spark-20261013)是可变的别名,发布方可以随时把同一个tag指向新的镜像构建;Digest(形如 sha256:...)则是镜像内容的哈希,一旦确定就不可更改。生产环境中用tag拉取,等于默认信任上游不会在你下次构建时替换内容;用digest拉取,才能保证今天验证过的组合明天仍是同一份二进制。

同样的原则适用于检查点:Hugging Face的main分支会移动,而完整的commit hash不会。本文的下载器和启动器都显式请求固定revision,正是为了让“我验证过的那套东西”和“下次部署跑的东西”是同一个。若省略这一步,容器和权重都可能在无人察觉的情况下发生漂移。

#本地部署决策API#Jev决策模型#Docker GPU推理#DGX Spark#开源LLM检查点#本地AI推理#决策头模型
分享:

相关推荐