Module 19:Productionizing — 让项目可用、可测、可解释
把原型整理为可使用、可测试、可解释的工程:logging、统计指标、CLI、可复现性、README 与开发者体验。
学习目标
- 理解 logging / tracing 在 GPU 推理系统中的作用。
- 设计结构化 stats:prefill/decode 时间、tok/s、显存、KV cache。
- 设计清晰 CLI:参数语义、默认值、错误信息、可复现 seed。
- 理解 deterministic greedy 与随机采样的可复现性边界。
- 知道一份 developer-facing README 应该包含哪些信息。
- 完成 Lab 19:新增一个 metric 和一个 CLI flag,并写文档。
19.1Logging / tracing:暴露运行状态
推理系统的故障通常发生在多个层次:模型文件、tokenizer、GPU device、KV cache、采样参数和输出流。只观察最终文本不足以定位问题;日志需要记录模型加载时间、GPU weights、prompt token 数、KV cache、prefill/decode 耗时、是否 hit EOS,以及是否启用 layer streaming。
xinfer 使用 Rust 的log与env_logger。用户通过环境变量控制日志级别:
$env:RUST_LOG="info"
xinfer generate --model models\Qwen2.5-0.5B-Instruct --prompt "Hello"
面向推理引擎的日志应满足以下要求:
- 关键事件可见:load、prefill、decode、EOS;
- 指标结构化:token 数、ms、tok/s、MiB;
- 不泄漏敏感数据;
- debug 级别能帮助开发,info 级别能帮助 用户。
19.2结构化统计:GenerateStats 与 memory reporting
xinfer 的 runtime 返回GenerateStats,记录:
| 字段 | 含义 | 用途 |
|---|---|---|
prompt_tokens | prompt token 数 | prefill 分母 |
generated_tokens | 生成 token 数 | decode 分母 |
prefill_ms | prefill 耗时 | 首 token 等待分析 |
decode_ms | decode 循环耗时 | 持续生成速度 |
CLI 根据这些字段计算:
模型侧还提供:
resident_weight_bytes()kv_cache_bytes(capacity)is_streaming()
这些指标把一次生成拆成可比较的部分:GPU weights 反映 resident 模型权重规模,KV cache 随 prompt 与生成上限增长,prefill 和 decode 分开报告后,可以区分长 prompt 的一次性成本与逐 token 延迟。
19.3干净 CLI:默认值、参数与错误信息
CLI 是用户接触引擎的稳定接口。xinfer当前提供device和generate两个命令;generate支持下列参数:
xinfer generate `
--model models\Qwen2.5-0.5B-Instruct `
--prompt "Explain gravity in one sentence." `
--max-tokens 64 `
--temperature 0.8 `
--top-k 40 `
--top-p 0.95 `
--seed 7 `
--stream-layers 4
CLI 参数设计应满足:
- 名字直接表达含义;
- 默认值安全且有用;
- 错误信息说明缺少哪个参数;
- 可复现参数(seed、greedy)明确;
- 性能/内存信息写到 stderr,生成文本写到 stdout,便于脚本处理。
生成文本是程序结果,适合 stdout;日志、耗时和显存统计是诊断信息,适合 stderr。这样用户可以把生成文本重定向到文件,而不会混入 benchmark 信息。
19.4可复现性:seed 与 deterministic greedy
Greedy decoding 在实现固定时具有确定性:同一模型、同一 prompt 和同一设备路 径下,argmax 应得到相同 token。随机采样还依赖 RNG;xinfer 在sampling.rs中使用小型 xorshift RNG,并通过--seed指定初始状态,使 temperature、top-k 和 top-p 实验可重复。
复现性仍有边界。GPU 浮点计算在不同硬件、驱动或 kernel 路径上可能产生细微差异;对采样而言,logit 的微小变化可能改变 top-k/top-p 候选集合或概率区间。因此性能报告应说明:
- 硬件;
- 模型;
- 采样参数;
- 是否 greedy;
- commit / 版本。
19.5Developer-facing README
README 的作用是让开发者快速判断项目能力、运行方式、测试方式和限制。xinfer 的 README 包含:
- 项目目标和 highlights;
- workspace layout;
- requirements;
- build / run / test 命令;
- CLI 参数;
- 性能与优化摘要;
- Xbox GDK 路径;
- limitations / future work。
Lab 19新增一个 metric 和一个 CLI flag
选择一个有诊断价值的 metric 和一个 CLI flag,实现后补充文档。例子:
- metric:每 token 平均 decode ms;
- metric:readback bytes/token;
- metric:streaming slot count;
- flag:
--json-stats输出机器可读统计; - flag:
--no-stream强制 resident; - flag:
--bench-only不打印 token,仅测性能。
验收要求:
- CLI help / README 说明新 flag;
- 新增 metric 不破坏 stdout/stderr 分离;
- 至少一个 测试或手动验证命令;
- 说明这个 metric 对定位哪类问题有帮助。
小结
本章把 productionizing 落到 xinfer 的现有工程界面。CLI 通过device和generate暴露主要能力,generate的参数包括--model、--prompt、--system、--max-tokens、--temperature、--top-k、--top-p、--seed、--stream-layers和--no-chat。运行时的GenerateStats只记录四个基础字段:prompt_tokens、generated_tokens、prefill_ms和decode_ms,吞吐由方法计算;模型侧另有 resident weight、KV cache 与 streaming 状态报告。可复现性方面,temperature 为 0 的 greedy 路径确定性最强,采样路径需要固定--seed,并在跨硬件比较时注明环境。README 则把这些约束、命令和限制集中呈现给开发者。
思考与练习
基础为什么日志和生成文本应该分流到 stderr / stdout?
因为两者用途不同。生成的 token 文本是程序的“正式输出”,应走 stdout,便于管道传递、重定向到文件、被其他程序消费。日志/进度/调试信息是辅助信息,走 stderr,这样不会污染 stdout 的纯净输出。分流后用户可以xinfer ... > out.txt只保存文本,同时仍在终端看到日志。
基础列出一个性能报告至少应包含的 5 个字段。
①prefill 时间(ms);②prefill token 数 / prefill tok/s;③decode tok/s(或每 token 平均延迟);④生成 token 数;⑤显存占用(resident weight bytes,可加 KV cache bytes)。再加上模型名/精度、采样设置作为上下文更完整。xinfer 的GenerateStats覆盖 token 数与 prefill/decode 时间,吞吐由prefill_tok_per_s()和decode_tok_per_s()计算。
进阶为什么同一个 seed 在非 greedy 采样下仍可能因硬件浮点差异导致输出不同?
非 greedy 采样按概率分布抽样,而分布来自 logits → softmax。不同硬件/驱动/kernel 的浮点累加顺序不同,会让 logits 有微小差异;经过 softmax 与阈值(top-k/top-p 边界)放大后,某个 token 可能恰好被纳入或排除,或抽样时落在不同区间。即使 RNG seed 相同(消费的随机数序列一致),被作用的概率分布略有不同,就可能选出不同 token,并在自回归中逐步放大分歧。greedy 取 argmax 时只要不出现并列就更鲁棒。
进阶设计--json-stats的输出 schema。
输出一行 JSON,便于机器解析,例如:
{ "prompt_tokens": 23, "generated_tokens": 64, "prefill_ms": 41.2, "decode_ms": 2070.0, "prefill_tok_s": 558.0, "decode_tok_s": 30.9, "resident_weight_bytes": 988282880, "kv_cache_bytes": 1572864, "streaming": false, "sampling": {"temperature":0.0,"top_k":0,"top_p":1.0}, "seed": 42 }
要点:字段名稳定、用数值类型而非字符串、把采样设置嵌套成对象、字节用整数、时间/吞吐分开。这样可直接喂给基准脚本聚合 mean/p95,或在 CI 里对回归告警。
挑战实现一个 benchmark 模式,运行 N 次并报告 mean / p50 / p95 decode latency。
实现:加--bench N标志。①先做 1–2 次 warmup(排除首次 kernel 编译/权重上传/缓存冷启动),不计入;②循环 N 次,每次用 GPU timestamp 或稳定计时器记录每个 decode token 的耗时,收集成一个延迟数组(可记录所有 token,或每次运行的平均 decode 延迟);③统计:mean = 总和/数;p50/p95 = 把数组升序排序后取第 与 个分位值;④打印 mean、p50、p95(ms/token)及对应 tok/s。
注意:固定 prompt、生成长度、采样设置与 seed;报告分位数而非仅均值,能揭示偶发卡顿(如提交/调度抖动)。可顺带输出 min/max 与样本数。这与 Module 15“先测量再优化”的方法论一致。