把一台机器 变成一群人的共同语言
CF芝一
2026年07月22日 16:48

序  /  写给十天之后的我们

黑客松真正压缩的,不只是时间

十天里,我们看见一台机器如何被点亮,也看见陌生人如何因为同一个问题,慢慢站到一边。

回头看,这段开发历程并没有一个戏剧化的起点。桌面上是一台 DGX Spark,服务器名 spark-9408,局域网地址 192.168.110.60;磁盘里躺着一个约 67 GB 的 Qwen3.6-35B-A3B,本地权重被切成 26 个 safetensors 分片。它们都是真实存在的,却还不是一项能被团队使用的能力。

我们要做的事情听上去很朴素:让模型通过 Docker 启动为 vLLM 服务,用 tmux 守住后台进程,再以 OpenAI 兼容接口交给其他人调用。可开发从来不是把几个名词连起来。它是权限、镜像、显存、端口、日志和人,在同一个时间窗口里互相校准。

这份“十日谈”不是部署命令的扩写,而是一次开发过程的回放:我们怎样缩小问题,怎样把失败留下,怎样把一个人的发现变成所有人的路径。技术决定系统能不能跑,人际连接决定团队能不能继续跑。

文字采用朋友圈式的个人观察与技术纪实语气:不把困难写成传奇,也不把协作写成口号。由于工作区未保留团队聊天记录和逐字原话,文中的对话与互动均为基于部署过程的叙事化整理,不代表逐字纪实。

十日路线图

01 边界

02 权重

03 接口

04 容器

05 权限

06 脚本

07 参数

08 守护

09 验证

10 连接

DAY 01  /  问题定义

先别急着跑,先把边界说清楚

第一天最重要的产出,不是一行代码,而是大家终于在说同一个问题。

黑客松很容易制造一种错觉:键盘越响,进度越快。第一天我们反而停下来,把问题边界钉在纸面上。当前登录用户是 Developer,模型却位于 /home/xsuper/models/Qwen3.6-35B-A3B;服务器名是 spark-9408,局域网地址是 192.168.110.60,机器只有一张 GPU。每一条看起来只是环境信息,后来都变成了关键变量。

我们没有一开始就追求完整产品。最先达成的共识,是做出一个最小但可信的基础设施闭环:本地模型可见、容器能用 GPU、vLLM 能监听端口、OpenAI 兼容接口能返回结果。只要这条链路成立,前端、Agent、工作流和演示就都有了落脚点。

团队里有人关心最终体验,有人盯着运行环境,有人想尽快看到模型回答。我们没有让这些视角互相争夺,而是把它们排成依赖关系:先可运行,再可连接,最后才是可展示。

当天的技术账| 确认服务器与局域网地址;确认单 GPU 对应 tensor-parallel-size=1;把验收定义为端到端接口可调用。

连接发生的地方| 当所有人开始使用同一组环境事实,讨论从“我以为”变成了“我们确认”。这是团队真正开始形成的时刻。

DAY 02  /  模型盘点

67 GB 不是一个数字,是系统对你的第一句提问

模型不是一个文件名。它是一组必须被完整识别、完整挂载、完整加载的事实。

第二天,我们面对的是一个约 67 GB 的模型目录。config.json、chat_template.jinja、generation_config.json,以及从 model-00001-of-00026.safetensors 到 model-00026-of-00026.safetensors 的权重分片,构成了服务能够启动的前提。

盘点模型看似保守,却避免了最昂贵的一类错误:在基础输入不完整时调参数。我们先确认目录,再确认 config.json,再统计权重分片。不是为了写一份漂亮清单,而是为了让启动失败时,排查从确定事实出发。

这一天也让我们意识到,工程里的“看见”不是用眼睛看见。普通用户看不到的目录,sudo 可能看得到;宿主机看得到的路径,容器若没有挂载也看不到。每多一层运行边界,就多一种“它明明在那里”的错觉。

当天的技术账| 确认约 67 GB 模型规模;核对 26 个 safetensors 分片;检查 config.json;计划把模型只读挂载到 /model。

连接发生的地方| 一个人报出“67 GB、26 个分片”,另一个人马上知道该验证什么。数字在这里不是炫技,是协作接口。

DAY 03  /  服务化构思

不是把模型跑起来,而是把能力交出去

模型回答一次,只是惊喜;接口稳定地回答,才是产品的起点。

第三天,思路从“运行模型”转向“服务化模型”。我们选 vLLM,不只是因为它能加载权重,更因为它提供 OpenAI 兼容接口。/v1/models 和 /v1/chat/completions 成为前后端、Agent 与模型之间的公共语言。

公共语言会降低团队的耦合。调用方不必知道权重在谁的 home 目录,不必理解 26 个分片,也不必进入 tmux。它只需要知道基础地址、模型名和请求格式。基础设施越复杂,向外暴露的契约越应该简单。

我们把 served-model-name 固定为 Qwen3.6-35B-A3B。这个动作很小,却让日志、请求和验收口径统一。黑客松时间短,命名不一致带来的沟通成本,往往比代码错误更隐蔽。

当天的技术账| 采用 vLLM OpenAI 兼容服务;固定 served-model-name;规划模型列表与对话两级验收;host 设为 0.0.0.0。

连接发生的地方| 当接口契约写下来,做界面的队友与做部署的队友第一次可以并行工作。连接不再依赖谁坐在谁旁边。

DAY 04  /  运行环境

容器不是魔法,它只是把复杂性装进可复现的盒子

真正可靠的环境,不是“我这里能跑”,而是别人能复现你为什么能跑。

第四天的主角是 Docker。我们先确认 Docker 版本,再让一个最小 Ubuntu 容器调用 nvidia-smi。这个测试把问题拆成两段:宿主机有没有 GPU,与容器能不能使用 GPU。只有第二段通过,vLLM 镜像才值得继续拉取。

最终选用的镜像是 nvcr.io/nvidia/vllm:26.05.post1-py3。固定镜像标签,是给未来的自己留证据。黑客松现场最怕“昨天还能跑”,而不写版本号等于主动放弃了复现线索。

容器启动时采用 --gpus all、--network host、--ipc host,并将模型目录以只读方式挂载。它们分别回答了算力、网络、进程通信和数据边界的问题。容器不是让问题消失,而是把问题放到更清晰的位置。

当天的技术账| 验证 Docker 版本;用容器内 nvidia-smi 验证 GPU 透传;固定 NVIDIA vLLM 镜像标签;模型目录只读挂载。

连接发生的地方| 有人负责拉镜像,有人同时整理启动参数。等待不再是空白时间,而是团队重新分工的窗口。

DAY 05  /  关键踩坑

最像模型问题的那一次,其实是权限问题

报错说“目录不存在”,但真正不存在的,是当前用户穿过那扇门的权限。

第五天出现了整段开发里最有代表性的报错:模型目录不存在。路径没有拼错,模型也确实在那里;使用 sudo ls 可以看到,脚本里的普通 test 却返回失败。

原因最终落在 Linux 权限边界上。当前用户是 Developer,模型属于 xsuper,并位于 /home/xsuper 下。即便目标目录本身存在,Developer 也可能因为无法穿过上级 home 目录而访问失败。错误信息描述的是现象,不是根因。

修复不是粗暴地放宽整个目录权限,而是让脚本中的目录检查、config.json 检查、权重统计和 Docker 操作使用 sudo。这样既尊重原有数据边界,又让部署路径成立。我们还把这个坑写进 SOP,因为如果一个坑只被某个人记住,它迟早会重新出现。

这一天的价值不在于“终于加了 sudo”。它训练了团队共同面对报错的方式:不争论谁的判断更像答案,先设计一个能区分假设的最小验证。普通 ls 与 sudo ls 的差异,就是那把钥匙。

当天的技术账| 对比普通访问与 sudo 访问;定位 /home/xsuper 的路径权限;sudo test 检查目录与配置;sudo find 统计权重分片。

连接发生的地方| 有人提出“路径会不会写错了”,有人坚持“模型刚刚还看见过”。两种意见都没有被否定,最终靠一组对照命令合在了一起。

DAY 06  /  工程化

把一次成功,写成下一次不需要运气的脚本

脚本最好的地方,不是自动化,而是它把团队做过的判断保存下来。

第六天,我们把零散命令收束成 start_qwen36_35b_vllm.sh。脚本用 set -Eeuo pipefail 让错误尽早暴露,集中声明模型目录、镜像、容器名、服务名、端口和日志路径。每个变量都是一处可审阅的决策。

启动前先做防御式检查:目录是否可访问、config.json 是否存在、权重分片是否为零、镜像是否已下载。不同失败对应不同退出码。它们让日志不只告诉我们“失败了”,也告诉接手的人“应该从哪一层开始查”。

同名旧容器会被清理,新的标准输出和错误输出同时追加到日志。脚本没有保留可能随 vLLM 镜像版本变化的 language-model-only、reasoning-parser、tool-call-parser 等扩展参数。我们的优先级很明确:先建立稳定基线,再讨论能力加法。

当天的技术账| 集中化部署参数;增加四级启动前检查;清理同名旧容器;日志写入 ~/vllm_logs;暂缓版本敏感扩展参数。

连接发生的地方| 当命令进入脚本,知识就从“谁记得”迁移到“谁都能检查”。这是一种很朴素的团队公平。

DAY 07  /  参数取舍

先把系统扶稳,再把性能推高

黑客松最稀缺的不是显存,是可以被验证的时间。

第七天的争论围绕参数展开。更长的上下文、更高的显存利用率、更多并发,看上去都更像一份强势答卷。但在单 GPU 的 DGX Spark 上,稳定启动比纸面极限更重要。

基线参数最终采用 tensor-parallel-size 1、dtype auto、gpu-memory-utilization 0.70、max-model-len 16384、max-num-seqs 1。它们不是性能终点,而是一组有意识的起点:先证明模型能够完整加载、服务能够监听、请求能够返回。

我们同时保留了降级路线。如果出现显存或统一内存不足,可以将 max-model-len 降至 8192,或将 gpu-memory-utilization 调至 0.60。真正专业的参数方案,不只写理想值,也提前写清退路。

团队在这一刻建立了共同的工程审美:演示不是把机器逼到墙角,而是在压力来临时仍有余地。克制不是保守,是在有限时间里保护确定性。

当天的技术账| TP=1;显存利用率基线 0.70;上下文长度基线 16384;并发序列基线 1;预设 8192 / 0.60 降级路线。

连接发生的地方| 有人想把参数拉满,有人担心启动失败。最后大家同意把“稳定可演示”当作共同胜利,而不是让某个单项数字赢。

DAY 08  /  运行守护

让服务留在后台,也让协作留在前台

tmux 做的事很简单:人可以离开窗口,进程不用跟着离开。

第八天,我们用 tmux 创建 qwen36_35b 会话,在其中启动部署脚本,再通过 Ctrl+B、D 脱离。这个动作解决了黑客松现场一个很实际的问题:终端连接不是服务生命周期。

tmux 不是编排平台,也不是生产级守护进程,但它适合十天里的我们。它足够透明:tmux ls 能看见会话,tmux attach 能回到现场,日志仍然在滚动。工具的价值不只在能力,还在它与阶段是否匹配。

Docker 容器和 tmux 会话形成两层观察面。docker ps 告诉我们容器是否存在,docker logs 展示容器输出,脚本日志保留连续记录,tmux 则让人重新进入启动现场。多一条观察路径,就少一次“它现在到底在不在”的猜测。

当天的技术账| 创建 qwen36_35b tmux 会话;后台保持 vLLM 运行;Docker 与 tmux 双层可观察;日志独立落盘。

连接发生的地方| 服务稳定留在后台后,部署者终于可以离开终端,去看队友怎样使用这条接口。技术角色之间的墙开始变薄。

DAY 09  /  端到端验证

没有证据的“成功”,只是一个心情

启动日志里出现一句 ready 不够。我们要从进程,一直走到一次真实回答。

第九天,我们把验证拆成四层。第一层看容器:docker ps 中是否出现 qwen36_35b_vllm。第二层看端口:ss 是否显示 0.0.0.0:8000。第三层看模型列表:/v1/models 是否返回 Qwen3.6-35B-A3B。第四层才是对话:向 /v1/chat/completions 发出请求,等待真实生成结果。

这四层故意从近到远排列。容器在,不代表端口在;端口在,不代表模型加载完成;模型列表能返回,也不必然代表生成链路正常。分层验证让故障范围快速收敛,也让每个人都能用相同证据判断状态。

我们还把日志排查写成固定入口:tail 最近 200 行,或者查看容器最近 200 行日志。不是所有人都需要从第一行读到最后一行。好的排障设计,会让最有信息量的部分先出现。

当天的技术账| 容器状态验证;8000 端口监听验证;模型列表验证;对话生成验证;保留最近 200 行日志入口。

连接发生的地方| 当大家围着同一次 API 返回结果确认“真的通了”,那不是某个人的成功。证据让喜悦变成了可以共享的事实。

DAY 10  /  能力交付

最后一天,模型走出服务器,团队走进彼此

真正的完成,不是机器会回答,而是另一个人不需要你在场,也能得到回答。

第十天,接口从 127.0.0.1 走向局域网。基础地址变成 http://192.168.110.60:8000/v1。只要调用设备与服务器在同一网络、防火墙没有拦截 8000 端口、vLLM 监听 0.0.0.0,其他队友就能使用同一模型能力。

到这里,我们交付的不只是一段部署结果,也是一组可接手的操作:怎样查看会话、怎样重新进入、怎样看容器和日志、怎样停服务、端口冲突时怎样查、内存不足时怎样降级、同目录下其他模型怎样发现。

黑客松常被讲成速度的比赛。但十天之后,我们更愿意把它理解成一次关系的加速:人与机器建立可观察的关系,前端与模型建立稳定的契约,队友之间建立不依赖口头转述的信任。那条 API 不只连接了软件模块,也连接了各自不同的专业背景。

我们没有把所有问题一次解决。reasoning parser、tool calling、更高并发、生产级守护与安全控制,都还在下一段路上。但我们有了一块稳定地基,也有了一种共同工作的方式:先把事实摊开,再把问题缩小;把偶然成功写成可重复路径,把个人发现留成团队资产。

当天的技术账| 公布局域网 OpenAI 兼容基础地址;补齐停止与恢复路径;记录端口与内存故障预案;支持发现同目录其他模型;形成可移交 SOP。

连接发生的地方| 最后的交付不是“我帮你跑起来了”,而是“这里有一条你可以自己走通的路”。这是技术给关系最好的礼物。

复盘  /  十天之后留下了什么

一条接口背后的四层共同语言

第一层是产品语言:调用方只关心 OpenAI 兼容协议、模型名和返回结果。第二层是网络语言:IP、端口、监听地址和防火墙定义能力能否被看见。第三层是运行语言:Docker、GPU 透传、共享内存、容器日志决定服务能否稳定存在。第四层是数据语言:模型路径、配置文件、权重分片与权限决定 vLLM 是否真的能够加载模型。

十天最重要的方法,是没有把四层揉成一个巨大问题。每次只问:我们现在在哪一层?有什么证据?下一条最小验证是什么?这套方法可以带走,继续用于更多模型、更多机器和更复杂的应用。

我们真正踩过的四个坑

表象

根因

带走的方法

目录不存在

Developer 无法穿过 /home/xsuper 的权限边界

用普通访问与 sudo 访问做对照,不根据报错字面猜根因

容器启动失败

GPU 透传、镜像、模型挂载或参数任一环节未满足

分别验证 Docker GPU、镜像存在、模型完整,再启动服务

服务似乎已启动

容器、端口、模型加载、生成链路并非同一状态

建立容器→端口→模型列表→对话的分层验收

参数越大越好

单 GPU 资源与黑客松时间不允许无限试错

先给稳定基线,再给清晰降级路线

方法  /  可以复用的开发原则

十条,比命令更值得保存的东西

01  先确认边界,再开始优化  用户、主机、GPU 数量、模型路径、网络范围,都是架构的一部分。

02  把模型服务化,而不是只把模型跑起来  稳定接口是跨角色协作的最小公共语言。

03  固定版本,让昨天可以被解释  镜像标签、模型名和关键参数必须留在可审阅的位置。

04  报错是现象,验证才接近根因  用最小对照实验区分权限、路径、容器和模型问题。

05  脚本是判断的容器  它应该保存前置检查、失败出口、日志路径和恢复方式。

06  基线优先于极限  先让单次生成稳定,再逐步增加上下文、显存占用和并发。

07  观察面越清晰,协作成本越低  容器、端口、模型列表、对话与日志应各有证据。

08  阶段匹配比工具豪华更重要  十天里的 tmux 可能比一套来不及验证的复杂编排更可靠。

09  把坑写下来,是对后来者的尊重  SOP 的意义是让同一个错误只付一次学费。

10  完成的标准是别人可以接手  当队友无需部署者在场也能调用、排查、停止和恢复,能力才真正交付。

黑客松结束时,留下来的不该只有一段演示视频。还应该有一套能让别人继续往前走的方法。

附录  /  技术底稿索引

可核验的部署事实

以下信息直接依据项目根目录中的《DGX Spark 上使用 Docker + tmux 部署 vLLM 的 SOP》整理,用于比赛成果归档与后续复现。

项目

已确认值

服务器

spark-9408;局域网地址 192.168.110.60

当前用户

Developer

模型目录

/home/xsuper/models/Qwen3.6-35B-A3B

模型规模

约 67 GB;26 个 safetensors 权重分片

服务镜像

nvcr.io/nvidia/vllm:26.05.post1-py3

容器 / 会话

qwen36_35b_vllm / qwen36_35b

监听方式

0.0.0.0:8000;Docker host network

模型服务名

Qwen3.6-35B-A3B

基础参数

TP=1;dtype=auto;GPU 利用率 0.70;上下文 16384;max-num-seqs=1

兼容接口

/v1/models;/v1/chat/completions

局域网基础地址

http://192.168.110.60:8000/v1

日志

~/vllm_logs/qwen36_35b_vllm.log

下一程  /  从能跑走向长期可用

这条路接下来可以怎样继续

十天完成的是可信基线,不是所有能力的终点。下一阶段仍然沿用同一套方法:一次只增加一个变量,为每一次增强保留证据、验收条件和回滚路径。

01  能力增强| 在稳定基线上逐项评估 reasoning parser、tool call parser 等版本敏感能力。每启用一项能力,都重新走完模型列表、基础对话与目标能力三层验证,避免把多个兼容性问题叠在一起。

02  运行治理| 将 tmux 方案升级为更稳定的服务守护与自动恢复机制,并补充健康检查、访问控制、日志轮转和异常告警。黑客松里的透明与简单要保留,运行责任则需要进一步明确。

03  性能画像| 基于真实调用负载测试上下文长度、并发、首 token 延迟与吞吐量,形成 DGX Spark 单机性能画像。参数优化不再依赖感觉,而是由同一批请求和同一组指标驱动。

04  故事补全| 补充团队成员、关键会议、原始对话与产品演示素材,把这份基于 SOP 的叙事化复盘升级为正式参赛纪实。让技术成果有证据,让每个真正参与的人也在文档里被看见。

下一步不是推翻这十天,而是沿着已经验证过的路径,把能力、稳定性和故事一起补完整。

 

 

 

 

 

十天以后

机器记得参数, 文档记得路径, 而我们记得彼此怎样把一个问题接住。

这就是 DGX Spark 黑客松“十日谈”的全部意义。 不是十天做完一切,而是十天之后,我们知道下一步可以一起去哪里。