Building an LLM Inference Engine from ScratchPart VI / Module 19
Part VI · Portability & Deployment

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 的logenv_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 级别能帮助用户。
Runtime eventsload / prefill / decode GenerateStatsms / tok/s Memory statsweights / KV cache User outputlogs + summary Productionizing 的第一步:把不可见的状态变成可观察的指标。
图 19-1:日志和指标把内部状态暴露出来,帮助调试和比较。

19.2结构化统计:GenerateStats 与 memory reporting

xinfer 的 runtime 返回GenerateStats,记录:

字段含义用途
prompt_tokensprompt token 数prefill 分母
generated_tokens生成 token 数decode 分母
prefill_msprefill 耗时首 token 等待分析
decode_msdecode 循环耗时持续生成速度

CLI 根据这些字段计算:

prefill tok/s=prompt_tokensprefill_ms/1000,decode tok/s=generated_tokensdecode_ms/1000\text{prefill tok/s} = \frac{\text{prompt\_tokens}}{\text{prefill\_ms}/1000}, \qquad \text{decode tok/s} = \frac{\text{generated\_tokens}}{\text{decode\_ms}/1000}

模型侧还提供:

  • resident_weight_bytes()
  • kv_cache_bytes(capacity)
  • is_streaming()

这些指标把一次生成拆成可比较的部分:GPU weights 反映 resident 模型权重规模,KV cache 随 prompt 与生成上限增长,prefill 和 decode 分开报告后,可以区分长 prompt 的一次性成本与逐 token 延迟。

19.3干净 CLI:默认值、参数与错误信息

CLI 是用户接触引擎的稳定接口。xinfer当前提供devicegenerate两个命令;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 分离

生成文本是程序结果,适合 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。
图 19-2:Productionizing 不是单一功能,而是一组让项目可用、可维护、可解释的工程能力。

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,仅测性能。

验收要求:

  1. CLI help / README 说明新 flag;
  2. 新增 metric 不破坏 stdout/stderr 分离;
  3. 至少一个测试或手动验证命令;
  4. 说明这个 metric 对定位哪类问题有帮助。

小结

本章把 productionizing 落到 xinfer 的现有工程界面。CLI 通过devicegenerate暴露主要能力,generate的参数包括--model--prompt--system--max-tokens--temperature--top-k--top-p--seed--stream-layers--no-chat。运行时的GenerateStats只记录四个基础字段:prompt_tokensgenerated_tokensprefill_msdecode_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 = 把数组升序排序后取第 0.50n\lceil0.50\cdot n\rceil0.95n\lceil0.95\cdot n\rceil 个分位值;④打印 mean、p50、p95(ms/token)及对应 tok/s。

注意:固定 prompt、生成长度、采样设置与 seed;报告分位数而非仅均值,能揭示偶发卡顿(如提交/调度抖动)。可顺带输出 min/max 与样本数。这与 Module 15“先测量再优化”的方法论一致。