@williamlouis 目前设备是DGX SPARK 不知道如何加这个副屏,而且还得学习一下副屏的调试。
-
给 DGX Spark 写了个零依赖实时监控面板:1 个 Python 文件 + 1 个 HTML 文件 -
蓝宝石 RX 7900 XTX 24G BLOWER 刷完固件后弹「系统设置更改」、BIOS 乱码,重装 Win11 后消失蓝宝石 RX 7900 XTX 24G BLOWER 刷完固件后弹「系统设置更改」、BIOS 乱码,重装 Win11 后消失
起因:新入手一块蓝宝石(Sapphire)RX 7900 XTX 24G BLOWER 涡轮版,到货后在现有 Win10 上装驱动、
跑 3DMark 压力测试通过率 98.4%,一切正常。
后来手贱去官网下了固件更新程序刷了一次固件——从此系统开始弹「系统设置更改」,
BIOS 里主显示输出被自动改成独显,BIOS 界面还有部分文字乱码。
重装 Win11 后提示确实没了,但固件那一版回不去了,现在想找同型号的旧固件备份都找不到。
在线等大佬支招。[系统设置更改提示]

目录
事情经过
楼主最近入手一块蓝宝石 RX 7900 XTX 24G BLOWER 涡轮版。到货后没有直接上生产力,先在现有的
Windows 10 专业版 22H2 系统里装好官方驱动,用 3DMark 跑了压力测试,通过率 98.4%,
当时显卡表现一切正常,没看出任何毛病。之后我想到蓝宝石官网有官方的固件更新程序,就下载下来给显卡更新固件。更新过程本身还挺顺利,
前后一共进安全模式刷了两次固件,两次都没报错、没花屏、没黑屏。问题出在刷完之后。
刷完固件后出了什么问题
一共三个现象:
-
系统弹「系统设置更改」
您的硬件设置已更改。请重新启动计算机,使这些更改生效。
进系统就弹,重启之后继续弹。
-
BIOS 里的主显示输出被自动改了
我原本设的是主板板载核显作为主显示输出。刷完固件进 BIOS 一看,这一项已经变成
7900 XTX 直连输出了——不是我改的,是刷完固件自己变的。 -
BIOS 设置界面部分文字乱码
这个当时没拍照,是我的失误。后来把显示输出切回板载核显,乱码就消失了,
所以现在也没法复现,只能靠描述。
[固件更新器:当前固件与是否需要更新]

已经试过的排查
# 手段 结果 1 多次重启电脑
「系统设置更改」照弹2 卸载并重装 AMD 官方驱动
无效3 BIOS 里手动把主显示改回板载核显
️ BIOS 乱码消失了,但「系统设置更改」依旧在4 联系厂家客服
答复两条路:要么返厂,要么自己备份旧固件重刷回去现在的状态
从刷完固件到决定重装系统,中间隔了大约一天时间。后来我换了一块 NVMe 固态,
全新安装 Windows 11,装好官方驱动之后,那个烦人的「系统设置更改」提示没有再出现过。在 Win11 下又跑了 2 轮 3DMark 压力测试,都能正常通过,通过率 97.9%
(比刷固件前的 98.4% 略低,属正常波动区间)。现在打开 AMD Radeon 固件更新器,检测到的当前固件版本是
113-X4257W-NB6,
「需要更新」一栏显示 No——也就是官网上的最新版本。[硬件详细信息:固件版本与设备 ID]

[软件和驱动程序详细信息]

[整机信息:GPU / 显存 / CPU / 内存]

求助
主要诉求:想请坛里正好在用蓝宝石 RX 7900 XTX 24G BLOWER 这块卡、并且还没更新过固件
(或还停留在更新前旧版本)的朋友,帮忙备份一份旧版固件(.rom 文件)给我,我打算按厂家的第二条路
尝试回刷。版本越接近出厂原版越好。顺便请教几个问题:
- 「系统设置更改」这个提示到底是什么触发的?有没有办法在不重装系统的前提下彻底关掉它?
- BIOS 界面文字乱码,到底是不是显卡新固件(带 GOP 字体)引起的?有没有类似案例?
- 显卡固件这种更新到底有没有必要?如果版本差别不大,值不值得冒险?
- 各位平时都是怎么备份显卡固件的?有没有推荐的工具或方法?
硬件与环境
项目 参数 显卡 SAPPHIRE AMD Radeon RX 7900 XTX GAMING BLOWSER 24GB 型号标识 SKU 11322-05 / PN 588-1E487-0005A 显卡固件(当前) 部件号 113-X4257W-NB6,版本 022.001.002.045,日期 2026/09/10 04:39 显存 24560 MB (24GB) GDDR6,20.00 Gbps / 960 GB/s 核心频率 / 总线 2070 MHz / PCI Express 4.0 x16 设备 ID 744C,修订 C8,厂商 1002 (AMD),子系统 E487,子系统厂商 1DA2 (蓝宝石) CPU 13th Gen Intel Core i7-13700,16 核 24 线程,2.11 GHz 内存 32 GB 操作系统 Win10 Pro 22H2(刷固件前)→ Win11(重装后) 驱动 AMD Software Adrenalin 26.20.15.12-261023a1-205499C 驱动版本 AMD Windows 驱动 32.0.32015.12012 / 2D 驱动 8.1.1.1634 图形 API DirectX 12.1 / Vulkan 1.4.359 / OpenCL 2.0 / OpenGL 不可用 FSR 2.4.0.4541 压力测试 3DMark:刷固件前 98.4% → 重装 Win11 后 97.9% 几个可能的技术解释
以下纯属楼主个人猜测,欢迎大佬指正:
-
「系统设置更改」更像是 Windows 在请求重启,而不是故障本身。
显卡固件 / 驱动更新后触发这个提示很常见,所以重装系统后消失是说得通的。 -
BIOS 乱码大概率不是主板的锅。
BIOS 界面在主显示走独显时,用的是显卡 ROM 里自带的 GOP 字体渲染的——
新固件带的 GOP 一变,部分字形就可能串码。这也能解释「切回核显输出后乱码立刻消失」。 -
显卡本身应该是好的。
Win11 下 3DMark 两轮都能过,说明固件更新没有把核心算力刷坏,
目前的矛盾主要集中在「固件版本 + 旧系统状态」的兼容性上。
踩坑清单(实战)
这几条都是真踩过的,写出来省得别人也踩一遍。
1. 刷固件前没有备份原版 .rom,代价就是只能返厂。
厂家客服的原话是「要么返厂,要么自己有备份固件就重刷旧固件」。
所有教程都会说「先备份」,但没人强调不备份的代价是整卡变砖风险自担。2. 关键证据没拍照,永久丢失。
BIOS 乱码当时没拍,等改回核显输出后乱码消失了,再也复现不了。
排障现场的第一动作应该是拍照,而不是先动手改设置。3. 在主力系统上直接刷固件,排障时说不清是谁的锅。
刷完固件后的驱动状态和旧 Win10 系统里残留的驱动状态纠缠在一起,
「系统设置更改」到底是固件引起还是驱动安装残留,根本分不开。
固件刷写最好放在干净的系统环境里做。4. 3DMark 通过率 98.4% 不等于显卡没问题。
通过率只是压力测试下的稳定性参考,98.4% 和 97.9% 都在「勉强可接受」区间,
不能拿来当「硬件健康证明」,也别拿它当刷固件前后的对比结论。5. 重装系统让故障「消失」,不代表故障被修好了。
重装 Win11 后提示不再出现,很容易误判成「已解决」,其实只是 Windows 侧的状态被清掉了,
固件那一版还老老实实待在卡上。6. 官网更新器显示「需要更新:No」≠ 你手上就是出厂版。
No的含义是「你当前版本已经是官网发布的最新版」,
刷之前那一版(更早的出厂固件)已经找不回来了——除非有人提前备份过。7. BIOS 设置项被固件静默改掉,是刷完固件后最容易漏掉的一类问题。
「主显示输出」变成独显这一条,是靠第一次进 BIOS 才注意到的。
刷完固件应该逐项核对一遍 BIOS 关键设置,别默认它还是原来那样。 -
-
给 DGX Spark 写了个零依赖实时监控面板:1 个 Python 文件 + 1 个 HTML 文件给 DGX Spark 写了个零依赖实时监控面板:1 个 Python 文件 + 1 个 HTML 文件
起因:在 DGX Spark 上跑大模型时,想随时看清 CPU / GPU / 统一内存 / 磁盘 / 网络 / 推理吞吐的真实状态,
以及推理引擎(推理服务)到底跑得怎么样。官方 Dashboard 看不了推理细节,nvtop/btop又看不到
LLM 侧的指标,索性自己写了一个。约束:零第三方依赖——后端只用 Python 标准库,前端单文件 HTML + 原生 JS,不引任何 CDN。
拷过去python3 monitor.py就能跑。[整体效果]

目录
它有什么
17 个面板,全部可拖动、可缩放、可换位置,布局存在浏览器本地,导出一段 JSON 就能在别的机器上复现。
顶部 9 张 KPI 卡带迷你走势线;中间 6 张曲线图支持鼠标悬停出十字线和数值气泡,Y 轴单位会在
B/s → KB/s → MB/s之间自动切换。[LLM 运维区]

上排是 推理引擎(running/waiting、解码速度、TTFT、TPOT、KV Cache 分池占用、MTP 接受率)、
容器 llm(CPU / 内存 / net / block IO)、一键测速(发一次真实对话,按usage计数算吞吐)。下排是硬件明细:CPU 每核负载(20 核 GB10)、温度传感器 / 风扇(带过热状态染色)、
磁盘 I/O、网络接口(含错误/丢包计数)。[硬件表格区]

再往下是 进程 Top、引擎指标明细表、当前生效配置(直接读容器里的环境变量,一眼看到
TF_SOLO_ROWS/TF_SPILL_GIB这些实际生效的参数)、容器日志尾部。[进程 / 引擎指标 / 生效配置]

交互细节
鼠标悬停在曲线上,十字线定位到具体时刻,气泡给出该时刻的各序列数值:
[悬停交互]

拖卡片标题栏换位置,拖右下角的小三角改大小。布局可以导出成 JSON:
[布局导出]

复制这段 JSON,粘到另一台机器上点「应用」,就是一模一样的排布。
架构
monitor.py ~34 KB Python 3 标准库,无第三方依赖 ├─ 采样线程 1 Hz 写环形缓冲(deque(maxlen=...)) ├─ HTTP 服务 ThreadingHTTPServer └─ API /api/snapshot 当前快照(含 host/cpu/memory/gpu/sensors/disk/network/processes/engine) /api/series 历史序列,?window=60|300|900|1800|3600 /api/stats snapshot + 容器 stats + uptime /api/config 容器镜像/状态/环境变量(docker inspect) /api/logs 容器日志尾部 ?tail=200 /api/probe 发一次真实对话,测 TTFT / decode 速度 /api/key 设置 API Key /api/healthz index.html ~49 KB 单文件:HTML + CSS + JS,无 CDN为什么服务端采样? 因为磁盘/网络的速率本质上就是「两次采样之间的差值」,没法在 HTTP 请求发生的
那一刻凭空算出来。所以有一个后台线程固定 1 Hz 采样写进deque,前端只是来取。
值得抄的 6 个实现细节
1. 速率只能用 delta 算,不能在请求时重新采样
这是最容易踩的坑。直觉写法是「收到请求 → 读一次
/proc/diskstats→ 和上次比」,但**「上次」存在哪**
是问题:如果请求比采样线程更频繁,就会拿两个几乎同时刻的采样点作差,差值被摊薄成一个极小的时间窗,
算出来的速率要么是 0,要么抖得没法看。正确做法是:后台线程独占采样,HTTP 只返回缓存的结果。
def disk_info(prev, dt): """prev 是上一次采样的 /proc/diskstats 快照,dt 是两次采样之间的时间差。""" for name, v in cur.items(): old = (prev or {}).get(name) if old and dt > 0: d["read_bps"] = round((v[2] - old[2]) * 512 / dt, 1) # 扇区数 × 512 = 字节 d["write_bps"] = round((v[6] - old[6]) * 512 / dt, 1) d["read_iops"] = round((v[0] - old[0]) / dt, 2)后台循环里
dt用真实经过时间:def sample_once(): now = time.time() dt = max(0.5, now - STATE["prev"].get("ts", now - SAMPLE_INTERVAL)) STATE["prev"]["ts"] = now2. 统一内存架构下,
nvidia-smi的显存字段是[N/A]GB10 的 CPU 和 GPU 共享同一块内存(标称 128 GB,系统可见约 122 GiB),
nvidia-smi对显存总量/占用直接返回[N/A]:$ nvidia-smi --query-gpu=memory.total,memory.used --format=csv [N/A], [N/A]所以面板里的「统一内存」读的是
/proc/meminfo,而不是显存字段;GPU 面板只取利用率/温度/功耗。
另外power.limit、fan.speed在这台机器上也是[N/A],别指望它们。顺带一个有用的取法:逐进程显存是可以拿到的(
--query-compute-apps),把它求和就得到
「模型常驻显存」:rc, so, _se = run(["nvidia-smi", "--query-compute-apps=pid,used_gpu_memory,process_name", "--format=csv,noheader,nounits"]) out["model_gpu_mib"] = round(sum(p["mem_mib"] for p in procs), 1)3. 用 12 栅格显式定位,而不是
auto-fit最初用
grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)),结果卡片宽度随窗口变化,
而且没法留空位、没法叠放。改成显式坐标后什么都解决了:.grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr)); grid-auto-rows:44px;gap:10px} .card{grid-column:span var(--w,3);grid-row:span var(--h,5)}每张卡片存
{x, y, w, h}(单位是栅格格数),JS 直接写:el.style.gridColumn = (c.x + 1) + " / span " + c.w; el.style.gridRow = (c.y + 1) + " / span " + c.h;显式坐标才能表达「第二行右边留空」和「磁盘 I/O 叠在网络接口下面」这种排布——自动排布是做不到的。
4. 卡片被压住时「就近挪开」,而不是跳到页面另一头
拖动时如果落点压住了别的卡片,需要把被压的挪走。朴素的实现是「从左上角扫描找第一个空位」,
结果卡片会莫名其妙飞到页面最上面。这里改成收集所有空位,再按离原位的距离排序:function findSpot(w, h, exceptId, prefer){ const free = []; for (let y = 0; y < 400; y++) for (let x = 0; x <= GRID.cols - w; x++){ const cand = {x, y, w, h}; let ok = true; for (const [id, c] of Object.entries(state)){ if (id === exceptId) continue; if (overlaps(cand, c)){ ok = false; break; } } if (ok) free.push({x, y}); } if (!free.length) return {x: 0, y: 0}; if (prefer) // 就近安置:离原位越近越优先 free.sort((a, b) => (Math.abs(a.x - prefer.x) + Math.abs(a.y - prefer.y)) - (Math.abs(b.x - prefer.x) + Math.abs(b.y - prefer.y))); return free[0]; }5. canvas 曲线:nice tick + 自动单位 + 悬停十字线
不用图表库,纯 canvas 画。三点值得注意:
Y 轴刻度取「好看」的步长,否则会出现
7.4993这种刻度:niceStep(range){ const raw = range / 4, mag = Math.pow(10, Math.floor(Math.log10(raw) || 0)); const n = raw / mag; return (n <= 1 ? 1 : n <= 2 ? 2 : n <= 2.5 ? 2.5 : n <= 5 ? 5 : 10) * mag; }单位自动切换:网络速率可能是 500 B/s 也可能是 200 MB/s,固定单位必然有一头显示成
0.00 MB/s或123456789 B/s。所以按量级挑单位:if (this.opts.autoUnit){ const m = Math.max(Math.abs(vmin), Math.abs(vmax)); if (m >= 1048576){ this._scale = 1048576; this._unit = "MB/s"; } else if (m >= 1024) { this._scale = 1024; this._unit = "KB/s"; } else { this._scale = 1; this._unit = "B/s"; } }悬停十字线:鼠标位置换算成时间,吸附到最近的采样点:
onMove(e){ const g = this._geo; const x = e.clientX - this.cv.getBoundingClientRect().left; this.hover = (x - g.padL) / g.pw; this.draw(); // 重画时画十字线 const t = g.t0 + (g.t1 - g.t0) * this.hover; for (const s of this.series){ const p = this.nearest(s.data, t); // 线性找最近点 ... } }6. 布局持久化 + JSON 导出/导入
位置存
localStorage,再提供导出/导入,这样「我摆好的版面」可以固化成一段文本:const LAYOUT_KEY = "sparkmon_layout_v3"; function readLayout(){ return {layout: JSON.parse(JSON.stringify(state)), win: WIN}; } function saveLayout(){ localStorage.setItem(LAYOUT_KEY, JSON.stringify(readLayout())); }导出的 JSON 长这样,一眼就能看出每张卡片的格子坐标:
{ "layout": { "card-cpu": { "x": 0, "y": 0, "w": 2, "h": 5 }, "card-engine": { "x": 0, "y": 5, "w": 6, "h": 5 }, "card-logs": { "x": 0, "y": 24, "w": 12, "h": 6 } }, "win": 300 }
部署
# 1. 放到机器上 scp -r dgx-monitor <user>@<SPARK_IP>:~/ # 2. 一键安装(注册 systemd 服务 + 自检) cd dgx-monitor && bash install.shinstall.sh做的事:把文件装到~/dgx/monitor/、用sed替换 systemd 单元里的
User=/WorkingDirectory=/MONITOR_PORT=、daemon-reload、enable --now,
最后 curl 一下首页确认真的起来了。自检脚本检查这几项,任何一项不过会明确指出:
$ bash verify.sh PASS 服务 spark-monitor active PASS 首页 200(48958 字节) PASS API /api/stats 200 PASS 新版前端已生效(找到栅格卡片) PASS 快照字段齐全(gpu/sensors/disk/network) PASS /api/config 200 PASS /api/logs 200 PASS /api/healthz 200 PASS llm :8888 /health ok也可以不用 systemd,直接前台跑:
MONITOR_PORT=8890 python3 monitor.py
踩坑清单(实战)
这几条都是真踩过的,写出来省得别人再踩一遍。
1. 一个没定义的函数,能让半张页面变成空白。
前端调了setBar()但从没定义过它,ReferenceError直接把loadSnapshot()从中间打断,
结果 GPU、传感器、磁盘、网络、进程、容器、引擎表全部没渲染,而页面看起来只是「没数据」。
教训:前端的渲染要分段,别让一个异常连坐后面所有模块;排查时先看 console 报错。2. 同名函数声明会互相覆盖。
布局管理里有个function render()画栅格,数据渲染里也有function render(s)画面板——
JS 里函数声明会被提升,后定义的那个赢,于是数据渲染被静默顶掉,页面全空但不报错。
后来把布局那个改名paintLayout()才解决。3. 轮询打爆后端日志。
监控自己每秒去拉推理引擎的/metrics,但没带 API Key,结果后端日志里刷了 1000 多条 401。
加了 API Key 还不够,又加了失败退避:if resp.status == 401: self._engine_blocked_until = now + 300.0 # 401 就安静 5 分钟4. 单位约定不一致,数值会离谱 1000 倍。
Prometheus 的time_to_first_token_seconds是秒,但另一套采集里换算成了毫秒,
两边混用就会看到「TTFT 均值 105952.685 s」。贴指标前先确认单位,表头也要写清楚。5. Y 轴标签被裁掉。
padL: 48放不下87.81 MB/s,首字符被切掉变成7.81 MB/s,看起来像数据错了。
标签宽度要按「最长刻度 + 单位」算。6.
install.sh用enable --now不会重启已在跑的服务。
enable --now对已运行的服务是 no-op,改完代码重新安装,线上跑的还是老版本。
正确做法是systemctl --user enable之后显式restart。7. systemd 单元里的
User=是写死的。
别人拿去用就会因为用户名不同而起不来。安装脚本里用sed替换成当前用户。
文件清单
dgx-monitor/ ├── monitor.py 后端(标准库,零依赖) ~34 KB ├── index.html 前端(单文件,无 CDN) ~49 KB ├── spark-monitor.service systemd 单元模板 ├── install.sh 一键安装 ├── verify.sh 部署后自检 └── layout.test.js 布局引擎单元测试layout.test.js是给布局逻辑写的单元测试(纯逻辑,Node 里跑,浏览器无关),
校验默认版面不重叠、关键排位、碰撞推挤、就近安置、持久化往返:$ node layout.test.js == 1. 默认版面(17 张卡片)自身不重叠 == PASS 全部卡片互不重叠 PASS 状态里有 17/17 张卡片 ... == 5. findSpot 返回真正空的位子 == PASS 空位 {"x":0,"y":30} 无冲突 PASS 带 prefer 取位不更远:{"x":8,"y":30}(距离 21 ≤ 扫描序 29) 全部通过 ✅怎么拿到源码
源码就上面这 7 个文件,没有构建步骤、没有依赖清单,全量 84 KB。几种给法:
- 贴到 gist / 仓库:只有 7 个文件,直接建个仓库传上去最省事
- 打包成 tar.gz:
tar czf dgx-monitor.tar.gz dgx-monitor/,随帖附上 - 从帖子直接抄:本文已经把最关键的 6 段实现贴出来了,照着写也能复现个七七八八
如果你想给别人的是「换个样式 / 换数据源」的版本,改
index.html里的CHARTS/DEFAULT_LAYOUT
两个常量就够了,数据源在monitor.py的sample_once()里。源码里的占位符
发布版把和具体环境绑定的部分换成了占位符,拿到后改这几处即可:
占位符 在哪 改成 <ENGINE_PREFIX>monitor.py(Prometheus 指标前缀,约 20 处)你引擎的前缀,例如 vllm:<model-name>monitor.py模型名;留空则自动从 /v1/models发现<user>spark-monitor.service不用手改, install.sh会自动替换成当前用户名
已知限制
- 面板不绑定任何推理后端。引擎相关的部分靠读
/metrics(推理服务 / vLLM 都行),
没起容器时那几块是空的,其余面板照常工作。 - API Key 只存在于内存或容器环境变量里,面板本身不落盘、不外传。
- 布局存在浏览器 localStorage,换浏览器要靠「布局…」里的 JSON 搬运。
- 采样精度受限于 1 Hz:突发写入(几百 MB 的 dd)能抓到,持续小流量会被平均掉。
- 只做了 Linux/aarch64,其它平台的采集函数(
/proc、hwmon)需要改。
本文配图与源码均已做脱敏处理:模型名、容器镜像 tag、引擎名、用户名、主机名、IP、API Key
均已替换为占位符或打码;面板本身也从不显示 API Key 的值。
dgx-monitor-post.zip -
DGX Spark + TensorFold/SparkFold 跑 Qwen3.8-Flash-Next 完整实战:EXL3 4.05bpw 由 18 个补丁到中文 draft 词表,decode 64.7 tok/s(附全套 benchmark)
为了方便在内网查看设备的实时是使用情况,让DSH写了一个网页。效果还不错。 -
DGX Spark + TensorFold/SparkFold 跑 Qwen3.8-Flash-Next 完整实战:EXL3 4.05bpw 由 18 个补丁到中文 draft 词表,decode 64.7 tok/s(附全套 benchmark) -
7900XTX选择困难症 -
求助帖!各位大神7900XTX的AMD公版卡可以入手吗?那么蓝宝石的涡轮卡有没有均热板呢?
-
7900XTX选择困难症我准备在淘宝下单时发现买完了,就里面问客服,追了好一会才又放出来。上一次7000没下单,这次7200才下单,真系嫌钱腥。
-
DGX Spark + TensorFold/SparkFold 跑 Qwen3.8-Flash-Next 完整实战:EXL3 4.05bpw 由 18 个补丁到中文 draft 词表,decode 64.7 tok/s(附全套 benchmark)DGX Spark + TensorFold/SparkFold 跑 Qwen3.8-Flash-Next 完整实战:EXL3 4.05bpw 由 18 个补丁到中文 draft 词表,decode 64.7 tok/s(附全套 benchmark)
沿着
dgx-spark-llm-serving-main仓库lanes/exl3-sparkfold这条路线,我在 DGX Spark 上从零跑了一遍完整 setup(2026-10-29 部署完成,服务在跑),顺便做了全套 benchmark 和中文 draft 词表的 A/B 实测。有几个发现(尤其中文词表那 part)想还回来给社群。一、硬件
- DGX Spark:GB10(sm_121a)、128 GB 统一内存、aarch64,Docker 带 NVIDIA runtime
- 根分区 3.7 T,这条 lane 共占 173 G:
~/models/qwen3.8-flash-next-exl3-4.05bpw 101 G 权重 ~/.cache/tensorfold-sparkfold 967 M 扩展缓存 + 视觉权重 ~/tensorfold-src 25 M 打补丁的源码树 ~/src ~70 M 上游配方 + TensorFold tarball /var/lib/docker ~70 G 镜像- 前置条件(
01-healthcheck.sh只读体检,不要跳过):可用统一内存 ≥ 121 GiB、磁盘可用 ≥ 200 GiB(权重 100 + 缓存 + spill 最多 48)、docker info能看到 NVIDIA runtime。
二、软件栈和安装
- 引擎:TensorFold 0.6.5 + SparkFold
806b345+ 仓库 18 个补丁。这条 lane 的全部价值来自那 18 个补丁——仓库记录过未打补丁时英文 decode 42.6 tok/s vs 打完 62.9,不要用未经补丁的 SparkFold 跑。 - 镜像:
tensorfold-qwen38:v0.3.6.3(MiaAI-Lab 封装)再叠一层xgrammar(response_format/ 结构化输出)→tensorfold-qwen38:v0.3.6.3-xgrammar(36.9 GB,tf.patches=c1f5d72f8d16)。 - 部署脚本全部做成幂等,失败重跑即可;都支持
DRY_RUN=1打印将执行的命令。
cd ~/dgx-spark-deploy && chmod +x *.sh bash 01-healthcheck.sh # 只读体检,先做 bash 02-build-tree.sh # SparkFold 806b345 + 18 补丁 → ~/tensorfold-src bash 03-build-image.sh # 两层镜像(v0.3.6.3 → v0.3.6.3-xgrammar) bash 04-download-pack.sh # 权重 99.94 GiB:钉 commit sha + 逐文件 size + SHA-256 校验 bash 05-vision.sh # 视觉权重转换(可选,VISION=0 就跳过) bash 05b-draft-vocab-zh.sh # 构造中文 draft 词表(中文场景建议做,见第七节) bash 06-start.sh # 启动(封装仓库的 start.sh,不重新实现启动逻辑) bash 07-smoke.sh # 冒烟 + 中英文基准(RUN_BENCH=1 附带仓库的 bench_en.py)全新机器先跑
bash 00-bootstrap.sh:检查并补齐 Docker / NVIDIA Container Toolkit /hfCLI,已装的一律跳过,最后一步跑 GPU 容器冒烟,确认容器里真能看到 GB10。
02-build-tree.sh末尾应显示spill.py 在位(补丁 0005)。
MiaAI-Lab 的prepare.sh第 3、4 步会下载该封装自己的 MLX checkpoint,本 lane 用不到;照跑是为了与上游保持一致。三、两个必改的关键配置(今次分享重点之一)
3.1 内存地板 6 GiB —— 不是随便定的
TensorFold 0.6.5 的 stream cache 规则:只有当宿主机
MemAvailable高于max(4 GiB, RAM/10)(本机 = 12.2 GiB)再加 2 GiB 时才允许增长,否则直接丢掉一条空闲对话的整个状态。而 Spark 服务这个模型时空载只有约 15 GiB 可用——于是一条长对话会把另一条挤掉,每一轮都变成冷 prefill。仓库的对照实验(三条对话轮流,16 token 回复):
内存地板 后面几轮从零重新 prefill 的次数 12.2 GiB(引擎默认) 8 轮里 7 轮(丢了 409 s 的 prefill) 6 GiB( TENSORFOLD_MEMORY_RESERVE_GIB=6)9 轮里 0 轮 代价:到 earlyoom SIGTERM 线(RAM 的 3% = 3.6 GiB)的余量从约 11 GiB 降到约 5 GiB。所以不要把 n-gram 表全锁住——仓库记录过
TENSORFOLD_PIN_EXTRA_GIB=12(锁 36 GiB)会让 earlyoom 直接 SIGKILL 掉服务。当前配置PIN_EXTRA_GIB=-6.2,让锁页量维持在 20-25 GiB。3.2 NVMe spill —— 救 evicted 的对话,不是加速
TF_SPILL_DIR=/cache/spill,只写 ≥ 16384 token 的状态,目录上限 48 GiB,LRU 淘汰。仓库实测:100k token 的状态约 1.5-1.8 GiB,写 0.30-0.40 s,读 0.26-0.45 s。它只在对话被挤掉时救命。内存够时它什么都不做,也不带来加速。仓库明确写:只在受控重放里验证过,没有在真实流量上验证过——当成便宜的保险,不要当成实测收益。
四、模型
turboderp/Qwen3.8-Flash-Next-exl3,分支4.05bpw_h6_ng6,EXL3 4.05 bpw,125B 总参 / 6B 激活,99.94 GiB。本机校验结果:26 个文件的 size + SHA-256 与 Hub 逐个一致,实下 100.08 GiB(trunk 62.9 GiB / n-gram 表 36.36 GiB / 视觉 0.52 GiB,与仓库记录逐项吻合)。
为什么必须逐文件校验:仓库的 vLLM lane 明确记录过——大小对但内容截断的 shard 能正常加载、报告出合理的 shape、产出量级正确的激活值,然后生成通顺但完全错误的输出。不崩溃,不报错,只是不对。有人为这个丢了一天。
另两个硬坑(与网络无关,但必须钉住版本):
- 上游封装仓库(MiaAI-Lab)的
main已换成 Zig 原生引擎,与本 lane 的 Python 引擎不兼容。必须用 commit38da375e5f4a0be0ed4c065d1014f61088e0a9e4(2026-09-29,最后一个 Python 引擎 + TensorFold v0.3.6.3 版本)。这个 commit 是对的,有硬证据:它的补丁集 hash 是c1f5d72f8d16,与仓库 README 记录的镜像标签tf.patches=c1f5d72f8d16一字不差。 04-download-pack.sh会先把分支名解析成 commit sha 再下载,钉住版本、可复现,并逐文件比对 size + SHA-256——照做就行。
五、生产启动配置(全套)
06-start.sh透传给仓库start.sh:变量 默认 说明 PROFILEsparksafe= 旧解码策略(A/B 对照用);bare= 引擎默认值SPILL1NVMe spill。 SPILL=0关掉VISION1多模态。 VISION=0关掉PARALLEL5并发流数 CONTEXT262144上下文长度 KV_DTYPEint8KV 精度 PORT8888端口 BIND127.0.0.1监听地址。 BIND=(空)= 所有网卡KNOBS— 追加任意 TF_*,如KNOBS="TF_KEEP=32"KNOBS="TF_DRAFT_VOCAB=draft_vocab_zh.txt" bash 06-start.sh # 中文词表(推荐) PROFILE=safe bash 06-start.sh # 对照组 VISION=0 SPILL=0 bash 06-start.sh # 纯文本、不落 NVMe BIND= bash 06-start.sh # 对外提供服务(服务没有 API key,不要暴露到不可信网络)启动日志里应该看到这四行:
lone-stream graph slot: 65536 rows spill: N stored states adopted ... GiB of pages locked <- 本机实测 "22.0 GiB of it locked"(仓库记录 20-25 GiB) serving qwen3.8-flash-next ... loaded in ~90s首次启动要编译 CUDA/Triton 扩展(本机 260 s),之后重启 73.8 s。脚本会等到
/health有响应(最多 15 分钟)。服务在127.0.0.1:8888。常用操作:
bash 07-smoke.sh # 冒烟 + 中英文基准 docker logs -f llm # 看日志 docker exec llm env | grep TF_DRAFT_VOCAB # 确认实际生效的词表日志目录
~/.cache/tensorfold-sparkfold/logs/。仓库的start.sh会docker rm -f旧容器、日志随之消失,所以06-start.sh现在重启前会先把旧日志备份进去。六、Benchmark(本机 2026-10-29 vs 仓库基线 2026-10-21)
Decode 单流: 英文 64.7 tok/s(69.12 / 68.72 / 55.50 / 65.54 四条提示均值)vs 仓库 62.9——同一套构建,吻合。中文装词表 53.4、不装 38.9(见第七节);仓库韩文基线 54.1。
并发: 仓库 sgbench 端到端 1 / 2 / 4 流 = 66.9 / 92.1 / 131.1 tok/s;llama-benchy decode(深度 0)1 / 2 / 4 流 = 61.7 / 89.5 / 112.2 tok/s。本机只测了单流,多流并发没有测。
Prefill(冷启动): 1.9k-49k token 真实文本 1,830-2,067 tok/s(仓库)。
Spill 还原: 100k 对话被挤掉后,冷启 40-50 s vs 从 NVMe 还原 0.8-2.3 s(仓库)。
工具调用: tool-eval-bench 90/100(装 xgrammar 后,仓库)。
关键校验(本机):
- 权重:26 个文件 size + SHA-256 逐个一致,100.08 GiB
- 源码树:18 个补丁提交全在,
draft_vocab_ko.txt86,232 行(与仓库记录一字不差) - 镜像:
tf.patches=c1f5d72f8d16(与仓库 README 记录一致) - 引擎启动:22.0 GiB locked、84 个图、spill 目录 48 GiB、视觉塔 0.84 GiB
没测的(诚实清单): 中文质量没有基线(仓库对韩文质量和长工具上下文质量也明确没测过,速度追平 ≠ 质量追平,需要自己评估);多流并发;视觉输入(权重转换好了、引擎起来了,但没实际发过图片)。
七、中文 draft 词表 A/B 实测(新发现)
背景:MTP 投机解码的 drafter 只能从一张候选 token 列表里挑下一个 token。引擎自带的那张列表来自 CPython 标准库和 id < 65,536:
覆盖 比例 代码 / Markdown token 99.3-99.5% 英文散文 token 93.9% 韩文 token 47.5% 中文比韩文更糟。 这张表基本全是 ASCII,而 Qwen 分词器里绝大多数汉字 token 的 id 都 ≥ 65,536——它们根本不在候选表里。drafter 看不见中文 token,中文的投机接受率会很低,而 decode 速率基本就是"不投机"的速度。
上游其实已经做了中文版(MiaAI-Lab 的语言补丁带
draft_vocab_zh.txt,22,088 个纯汉字 token id),但那是替换语义——上游 README 明写 "English and code get a little slower with it"。本 lane 的TF_DRAFT_VOCAB是并集语义(仓库补丁 0009 的韩文表就是这么做的,补齐后英文贪心回复逐字节不变)。所以05b-draft-vocab-zh.sh取的是 并集:引擎默认表 ∪ 上游汉字 id = 101,679 个 id(默认 79,591 + 汉字 22,088)。中文拿到全部候选,英文和代码不受影响。A/B(同一套构建、同一批中文提示、同一份 server 配置,只换
TF_DRAFT_VOCAB):词表 ZH-HIST ZH-SCI ZH-ADV 均值 draft_vocab_ko.txt(仓库默认,不含汉字)34.38 42.80 39.52 38.90 draft_vocab_zh.txt(默认表 ∪ 汉字)50.40 55.96 53.68 53.35 +37.1%。 仓库测韩文时是 42 → 49(+17%),中文的增幅更大——因为默认表里汉字的覆盖率比谚文还低。旁证:每 chunk 的 token 数从 1.20-1.59 升到 1.88-2.11,也就是 MTP 接受率上去了,这正是 draft 词表该起的作用。对照仓库韩文基线 54.1:中文词表装上之后,中文追平韩文。
基准方法(
tools/bench_zh.py,与仓库bench_ko.py同构):走/v1/chat/completions,不覆盖采样参数(用服务端默认),thinking 打开,三条中文长句提示(历史散文 / 科学解释 / 现实建议),600 token 是测量窗口不是服务上限,流式测量、用首末 chunk 之间的时间把 decode 从 prefill 里分出来。复现:
cd ~/dgx-spark-deploy # A:仓库默认(不含汉字) KNOBS="TF_DRAFT_VOCAB=draft_vocab_ko.txt" bash 06-start.sh BENCH_MODEL=qwen3.8-flash-next BENCH_URL=http://127.0.0.1:8888/v1/chat/completions \ python3 tools/bench_zh.py # B:中文并集表 KNOBS="TF_DRAFT_VOCAB=draft_vocab_zh.txt" bash 06-start.sh BENCH_MODEL=qwen3.8-flash-next BENCH_URL=http://127.0.0.1:8888/v1/chat/completions \ python3 tools/bench_zh.py要注意的取舍:
- 只影响速度,不影响质量。 draft 出来的 token 要经过验证才被采纳,换词表不会改变回复内容(仓库对韩文验证过"英文贪心回复逐字节相同")。
- 一次只认一个词表。
TF_DRAFT_VOCAB是一个文件名,韩文表和中文表不要同时用;中文表取的是并集,换过去不会丢掉英文/代码的覆盖。 06-start.sh会把仓库默认的韩文表和 KNOBS 一起传给 docker,同名变量以最后一个为准;用docker exec llm env | grep TF_DRAFT_VOCAB确认实际生效值。
八、陷阱清单
- 未打补丁的 SparkFold 千万别跑:英文 decode 42.6 vs 打完补丁 62.9,差一半。
- 权重必须逐文件 SHA-256 校验:截断的 shard 不崩溃、不报错,只是生成"通顺但完全错误"的输出。
- n-gram 表千万别全锁:
TENSORFOLD_PIN_EXTRA_GIB=12(锁 36 GiB)→ earlyoom 直接 SIGKILL 服务。 set -e+pipefail会让脚本静默死掉:grep没匹配 → 退出 1 → 管道非 0 → 脚本被杀,什么错误信息都没有。07-smoke.sh就这么死过一次(引擎的锁页日志是 "22.0 GiB of it locked",不是仓库旧版的 "pages locked")。凡是grep ... | ...形式的输出行,后面都要|| true。- 上游封装仓库的
main已换成 Zig 引擎:必须钉 commit38da375e…(补丁集 hashc1f5d72f8d16对得上镜像标签,是硬证据)。 - 仓库
start.sh会docker rm -f旧容器,日志随之消失——06-start.sh已改成先把日志备份到$CACHE/logs/再重启。 - 第一条回复特别慢是正常的:另一个 graph 槽位首次解码要多花 0.6-1.6 s,第二次就正常。
- 本 lane 不主打 prefix caching:重复前缀不保证命中,与 vLLM lane 不同。
- 服务没有 API key:
BIND=对外监听只可以在可信局域网里用。 - 对话每轮都重新 prefill 多半是内存地板问题:看日志里
memory gate evicted ...,确认TENSORFOLD_MEMORY_RESERVE_GIB=6。
致谢
dgx-spark-llm-serving仓库(lanes/exl3-sparkfold)的前辈记录、turboderp 的 EXL3 权重、MiaAI-Lab 的封装与上游语言补丁、TensorFold / SparkFold。中文质量评估如果有人焗到更深,欢迎交流。 -
7900XTX选择困难症 -
7900XTX选择困难症PDD店家说这个3年保修是标错的,都是统一1年店保没有官保。随后果断淘宝,淘宝2年的是官保。
-
7900XTX选择困难症

本人目前的硬件配置是:CPU 为 i7-13700,主板为微星 PRO B660M-A WIFI DDR4,内存 32G DDR4 3200,显卡为 TESLA P4 8G。计划先入手一张 7900 XTX,再增加内存,用于本地部署,借此多学习、提升技能。想请教各位大佬:淘宝目前 7200 元、保修两年,拼多多 7400 元、保修三年,选哪个更好?