文章分类与统计

<p>已发布文章总数:324 <br />最近一次发布日期:2026-07-08</p> <table> <thead> <tr> <th style="text-align: left">Category</th> <th style="text-align: left">Last Updated Date</th> <th style="text-align: left">Count</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><a href="https://blog.wohin.me/categories/%e9%9a%8f%e7%ac%94%e4%b8%8e%e8%af%97" target="_blank" rel="noopener">随笔与诗</a></td> <td style="text-align: left">2025-02-09</td> <td style="text-align: left">103</td> </tr> <tr> <td style="text-align: left"><a href="https://blog.wohin.me/categories/%e6%bc%8f%e6%b4%9e%e7%a0%94%e7%a9%b6" target="_blank" rel="noopener">漏洞研究</a></td> <td style="text-align: left">2025-12-02</td> <td style="text-align: left">56</td> </tr> <tr> <td style="text-align: left"><a href="https://blog.wohin.me/categories/%e6%94%bb%e9%98%b2%e5%ae%9e%e6%88%98" target="_blank" rel="noopener">攻防实战</a></td> <td style="text-align: left">2022-10-20</td> <td style="text-align: left">44</td> </tr> <tr> <td style="text-align: left"><a href="https://blog.wohin.me/categories/%e6%b7%b7%e6%9d%82%e5%88%86%e7%b1%bb" target="_blank" rel="noopener">混杂分类</a></td> <td style="text-align: left">2026-01-26</td> <td style="text-align: left">33</td> </tr> <tr> <td style="text-align: left"><a href="https://blog.wohin.me/categories/%e4%ba%91%e8%ae%a1%e7%ae%97%e4%b8%8e%e5%ae%89%e5%85%a8" target="_blank" rel="noopener">云计算与安全</a></td> <td style="text-align: left">2022-09-19</td> <td style="text-align: left">28</td> </tr> <tr> <td style="text-align: left"><a href="https://blog.wohin.me/categories/%e7%a8%8b%e5%ba%8f%e5%88%86%e6%9e%90" target="_blank" rel="noopener">程序分析</a></td> <td style="text-align: left">2024-04-20</td> <td style="text-align: left">21</td> </tr> <tr> <td style="text-align: left"><a href="https://blog.wohin.me/categories/%e8%af%bb%e4%b9%a6%e7%ac%94%e8%ae%b0" target="_blank" rel="noopener">读书笔记</a></td> <td style="text-align: left">2023-11-04</td> <td style="text-align: left">12</td> </tr> <tr> <td style="text-align: left"><a href="https://blog.wohin.me/categories/%e6%93%8d%e4%bd%9c%e7%b3%bb%e7%bb%9f" target="_blank" rel="noopener">操作系统</a></td> <td style="text-align: left">2023-03-22</td> <td style="text-align: left">10</td> </tr> <tr> <td style="text-align: left"><a href="https://blog.wohin.me/categories/%e7%bc%96%e7%a8%8b%e8%af%ad%e8%a8%80" target="_blank" rel="noopener">编程语言</a></td> <td style="text-align: left">2019-08-10</td> <td style="text-align: left">8</td> </tr> <tr> <td style="text-align: left"><a href="https://blog.wohin.me/categories/%e4%ba%ba%e5%b7%a5%e6%99%ba%e8%83%bd" target="_blank" rel="noopener">人工智能</a></td> <td style="text-align: left">2026-07-08</td> <td style="text-align: left">6</td> </tr> <tr> <td style="text-align: left"><a href="https://blog.wohin.me/categories/%e6%91%84%e5%bd%b1" target="_blank" rel="noopener">摄影</a></td> <td style="text-align: left">2023-10-02</td> <td style="text-align: left">3</td> </tr> </tbody> </table>

2026/7/11
阅读更多

大语言模型推理服务知识梳理

<h2 id="0-总览">0. 总览</h2> <p>这是我和 AI 智能体共同探讨写作的一篇博文,用来帮助梳理大语言模型推理服务的相关知识与实践。这篇文章的主线是:<strong>一个开源模型如何从社区里的文件,变成服务器上可被业务调用的 LLM API 服务</strong>。这条链路看似只是“下载模型、启动服务、发请求”,实际涉及模型文件格式、GPU 显存管理、请求调度、KV cache、并发、量化、性能指标等多个层次。</p> <p>这些问题琐碎、复杂,又各有各的重要。这让我想到了在“上古”时代,大家热衷于探索计算机中“从加电到系统启动”、“从 fork 到子进程执行”、“从输入一个 URL 到浏览器完成页面渲染”等水面以下鲜为人知的过程。</p> <p>庐山烟雨浙江潮。在人类把越来越多的工作——交付也好、让位也罢——给 AI 智能体的当下和未来,愿我们能始终保留探索、理解和表达的渴望。</p> <h3 id="01-什么是-llm-inference">0.1 什么是 LLM inference</h3> <p>LLM inference 指的是:给一个已经训练好的语言模型输入一段文本,让它继续预测后续 token 的过程。它和 training 的区别在于,inference <strong>不再更新模型参数</strong>,只做前向计算;training 则要计算损失、反向传播并更新权重。</p> <p>以“北京是中国的”这个 prompt 为例,模型不会一次性“写出答案”,而是做下一个 token 预测:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>输入:北京是中国的 </span></span><span style="display:flex;"><span>预测下一个 token:首 </span></span><span style="display:flex;"><span>输入:北京是中国的首 </span></span><span style="display:flex;"><span>预测下一个 token:都 </span></span><span style="display:flex;"><span>输入:北京是中国的首都 </span></span><span style="display:flex;"><span>预测下一个 token:。 </span></span></code></pre></div><p>这就是自回归生成(autoregressive generation)。绝大多数文本 LLM 都基于 <a href="https://arxiv.org/abs/1706.03762" target="_blank" rel="noopener">Transformer</a> 架构中的 causal decoder:每个位置只能看见自己之前的 token,因此天然适合“根据上文预测下一个 token”。</p> <h3 id="02-inferenceservingchat-completiongeneration-分别指什么">0.2 inference/serving/chat completion/generation 分别指什么</h3> <p>这几个词经常混用,但它们处在不同抽象层:</p> <table> <thead> <tr> <th style="text-align: left">术语</th> <th style="text-align: left">关注点</th> <th style="text-align: left">简单理解</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">inference</td> <td style="text-align: left">模型计算</td> <td style="text-align: left">把 token 喂给模型,得到下一个 token 的概率分布</td> </tr> <tr> <td style="text-align: left">generation</td> <td style="text-align: left">生成算法</td> <td style="text-align: left">根据概率分布和采样策略,决定真正输出哪个 token</td> </tr> <tr> <td style="text-align: left">serving</td> <td style="text-align: left">工程系统</td> <td style="text-align: left">把模型包装成可并发访问、可监控、可扩缩容的服务</td> </tr> <tr> <td style="text-align: left">chat completion</td> <td style="text-align: left">API 形态</td> <td style="text-align: left">接收 <code>system/user/assistant</code> 消息,返回 assistant 回复</td> </tr> </tbody> </table> <p>举个例子:<code>temperature=0.7</code>、<code>top_p=0.9</code> 属于 generation;<code>/v1/chat/completions</code> 属于 API;batching、排队、限流、metrics 属于 serving;矩阵乘法、attention、KV cache 属于 inference 内部计算。</p> <p>现代开源 serving 框架通常会兼容 <a href="https://platform.openai.com/docs/api-reference/chat" target="_blank" rel="noopener">OpenAI Chat Completions API</a>,这样上层应用可以用同一套 SDK 切换云模型和自部署模型。</p> <h3 id="03-模型从社区到-api-服务经历哪些环节">0.3 模型从社区到 API 服务经历哪些环节</h3> <p>一个典型 lifecycle 可以拆成下面几步:</p> <div class="mermaid">flowchart LR A["选择模型"] --> B["下载模型"] B --> C["准备环境"] C --> D["加载权重和 tokenizer"] D --> E["分配显存与 KV cache"] E --> F["启动 serving runtime"] F --> G["暴露 API"] G --> H["接收并调度请求"] H --> I["Prefill"] I --> J["Decode"] J --> K["流式或一次性返回"] K --> L["监控与压测"] </div> <p>其中最容易被低估的是中间三步:<strong>加载、分配、调度</strong>。一个模型能否跑起来,首先取决于权重和 KV cache 能否放进显存;一个服务能否跑得好,则取决于调度器能否在吞吐和延迟之间做合理权衡。</p> <p>例如,同样是 8B 模型,单人本地聊天时可能很流畅;一旦放到线上同时接收 100 个长上下文请求,瓶颈就可能从“模型权重放不下”变成“KV cache 被并发请求撑爆”。</p> <h3 id="04-vllmollamasglangtgillamacpp-分别解决什么问题">0.4 vLLM/Ollama/SGLang/TGI/llama.cpp 分别解决什么问题</h3> <p>这些工具都能“跑模型”,但设计目标不同:</p> <table> <thead> <tr> <th style="text-align: left">工具</th> <th style="text-align: left">更适合的场景</th> <th style="text-align: left">核心关键词</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><a href="https://github.com/vllm-project/vllm" target="_blank" rel="noopener">vLLM</a></td> <td style="text-align: left">GPU 服务端高吞吐推理</td> <td style="text-align: left"><a href="https://arxiv.org/abs/2309.06180" target="_blank" rel="noopener">PagedAttention</a>、continuous batching、OpenAI-compatible server</td> </tr> <tr> <td style="text-align: left"><a href="https://github.com/ollama/ollama" target="_blank" rel="noopener">Ollama</a></td> <td style="text-align: left">本地开发、个人电脑、快速体验</td> <td style="text-align: left">模型管理、<a href="https://docs.ollama.com/modelfile" target="_blank" rel="noopener">Modelfile</a>、本地 REST API、GGUF</td> </tr> <tr> <td style="text-align: left"><a href="https://github.com/sgl-project/sglang" target="_blank" rel="noopener">SGLang</a></td> <td style="text-align: left">复杂推理程序、高性能服务、结构化输出</td> <td style="text-align: left"><a href="https://arxiv.org/abs/2312.07104" target="_blank" rel="noopener">RadixAttention</a>、structured generation、OpenAI-compatible API</td> </tr> <tr> <td style="text-align: left"><a href="https://github.com/huggingface/text-generation-inference" target="_blank" rel="noopener">TGI</a></td> <td style="text-align: left">Hugging Face 生态中的生产化 serving</td> <td style="text-align: left">Rust router、streaming、metrics、HF 集成</td> </tr> <tr> <td style="text-align: left"><a href="https://github.com/ggml-org/llama.cpp" target="_blank" rel="noopener">llama.cpp</a></td> <td style="text-align: left">CPU/端侧/消费级设备</td> <td style="text-align: left">C/C++、GGUF、低比特量化、本地运行</td> </tr> </tbody> </table> <p>截至 2026 年 7 月,Hugging Face 的 <a href="https://github.com/huggingface/text-generation-inference" target="_blank" rel="noopener">Text Generation Inference</a> 仓库已在 2026-03-21 归档并进入 maintenance mode;它仍然值得学习,因为很多 serving 概念如 continuous batching、SSE streaming、Prometheus metrics 都非常典型,但新项目选型通常会优先看 vLLM、SGLang 或专用后端。</p> <h2 id="1-模型来源与命名">1. 模型来源与命名</h2> <h3 id="11-开源模型通常发布在哪里">1.1 开源模型通常发布在哪里</h3> <p>开源或 open-weight 模型通常发布在以下地方:</p> <ul> <li><a href="https://huggingface.co/docs/hub/en/models-the-hub" target="_blank" rel="noopener">Hugging Face Model Hub</a>:目前最主流的模型托管平台,模型通常以一个 Git 仓库形式存在,包含权重、配置、tokenizer、model card 等。</li> <li><a href="https://modelscope.cn/docs/models/intro" target="_blank" rel="noopener">ModelScope</a>:国内常用模型社区,访问和下载速度对国内服务器更友好。</li> <li><a href="https://ollama.com/library" target="_blank" rel="noopener">Ollama Library</a>:面向 Ollama 的模型分发入口,用户可以用 <code>ollama pull qwen2.5</code> 这类命令直接拉取。</li> <li>项目方官网或 GitHub release:例如 <a href="https://github.com/ggml-org/llama.cpp" target="_blank" rel="noopener">llama.cpp</a> 生态中的 GGUF 文件经常由社区作者转换后发布在 Hugging Face。</li> </ul> <p>Hugging Face 上的模型名一般是 <code>组织名/仓库名</code>,例如 <a href="https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct" target="_blank" rel="noopener"><code>Qwen/Qwen2.5-Coder-7B-Instruct</code></a>。这个名字不是随便起的,它同时传达了模型家族、版本、能力方向、规模和训练阶段。</p> <h3 id="12-模型名字怎么看">1.2 模型名字怎么看</h3> <p>以 <code>Qwen/Qwen2.5-Coder-7B-Instruct</code> 为例,可以拆成几段:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Qwen / Qwen2.5 - Coder - 7B - Instruct </span></span><span style="display:flex;"><span>组织 家族版本 能力方向 参数规模 指令微调版本 </span></span></code></pre></div><p>常见字段含义如下:</p> <table> <thead> <tr> <th style="text-align: left">字段</th> <th style="text-align: left">含义</th> <th style="text-align: left">例子</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">模型家族</td> <td style="text-align: left">基础架构和训练路线</td> <td style="text-align: left">Llama、Qwen、Mistral、Gemma、DeepSeek</td> </tr> <tr> <td style="text-align: left">版本号</td> <td style="text-align: left">同一家族的迭代</td> <td style="text-align: left">Llama 3.1、Qwen2.5、Mistral v0.3</td> </tr> <tr> <td style="text-align: left">参数规模</td> <td style="text-align: left">参数数量近似值</td> <td style="text-align: left">7B、8B、14B、32B、70B</td> </tr> <tr> <td style="text-align: left">能力方向</td> <td style="text-align: left">训练数据或任务偏向</td> <td style="text-align: left">Coder、Math、Vision、Reasoning</td> </tr> <tr> <td style="text-align: left">训练阶段</td> <td style="text-align: left">是否面向对话和指令</td> <td style="text-align: left">Base、Chat、Instruct</td> </tr> <tr> <td style="text-align: left">精度/量化</td> <td style="text-align: left">权重存储格式</td> <td style="text-align: left">BF16、FP16、INT8、AWQ、GPTQ、GGUF</td> </tr> </tbody> </table> <p><code>Base</code> 模型像“只读了大量书但没学会按人类指令办事的学生”;<code>Instruct</code> 或 <code>Chat</code> 模型则经过指令微调或偏好对齐,更像“知道如何回答问题的助教”。做服务时,如果目标是聊天、问答、代码助手,通常优先选 Instruct/Chat;如果要继续训练、做领域适配或研究模型行为,Base 更合适。</p> <p><code>Coder</code>、<code>Math</code>、<code>Vision</code>、<code>Reasoning</code> 则描述能力方向,不是训练阶段。比如 <a href="https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct" target="_blank" rel="noopener"><code>Qwen2.5-Coder</code></a> 这类模型通常在代码数据、单文件补全、debug、仓库级代码理解、工具调用等任务上做了更强训练;它仍然可以同时是 <code>Instruct</code> 模型。也就是说,<code>Coder</code> 回答“擅长什么”,<code>Instruct</code> 回答“是否学会按指令对话”。</p> <p><code>8B</code> 表示大约 80 亿参数。参数越多通常能力越强,但显存、延迟、吞吐成本也更高。粗略估算,BF16/FP16 每个参数 2 字节,8B 权重仅权重就约 16 GB;70B 则约 140 GB,还不包括 KV cache 和 runtime buffer。</p> <p>还有一类越来越常见的命名是 <code>35B-A3B</code>、<code>235B-A22B</code>。这里的 <code>A</code> 通常表示 <strong>activated parameters</strong>,常见于 <a href="https://arxiv.org/abs/1701.06538" target="_blank" rel="noopener">Mixture-of-Experts</a>(MoE)模型。例如官方 <a href="https://huggingface.co/Qwen/Qwen3.5-35B-A3B" target="_blank" rel="noopener"><code>Qwen3.5-35B-A3B</code></a> model card 标注为 35B total、3B activated:总参数约 35B,但每个 token 只路由到其中一部分专家,实际参与该 token 计算的参数约 3B。注意这不等于显存只需要装 3B 参数;如果不做专家 offload 或分布式切分,服务端仍然要管理完整专家权重。</p> <h3 id="13-选择模型前要问什么">1.3 选择模型前要问什么</h3> <p>选模型不是“越大越好”,而是把任务、硬件、性能目标三者对齐。可以先问五个问题:</p> <ol> <li>任务是什么:聊天、代码、RAG、摘要、分类、agent、批量离线生成,还是 embedding?</li> <li>输入和输出多长:短问答和 128K 长上下文 RAG 对系统的压力完全不同。</li> <li>硬件是什么:单张 24 GB 消费卡、A100 80 GB、H100、多机多卡,还是 CPU/Apple Silicon?</li> <li>服务目标是什么:更在意 TTFT、输出吞吐、P99 延迟、成本,还是质量?</li> <li>框架是否支持:模型架构、量化格式、chat template、context length 是否被 vLLM/SGLang/Ollama 支持?</li> </ol> <p>一个实际选择例子:如果要在一张 24 GB GPU 上搭一个代码助手,可以先考虑 7B/8B 级别的 <a href="https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct" target="_blank" rel="noopener"><code>Qwen2.5-Coder</code></a> 或同级模型;如果是公司内部 RAG 问答,可能更关注长上下文、中文能力和吞吐;如果是本地离线使用,Ollama + GGUF 量化模型可能比完整服务端框架更省心。</p> <h3 id="14-上下文窗口怎么看">1.4 上下文窗口怎么看</h3> <p>上下文窗口(context window)是模型一次生成时能“看见”的 token 总预算,通常包括输入 prompt、历史对话、工具返回、RAG 检索片段,以及本次将要生成的输出。一个 <code>128K context</code> 模型并不是说可以输入 128K token 后再无限输出,而是输入和输出共享这块预算。</p> <p>$$ N_{\text{input}} + N_{\text{output}} \leq N_{\text{context}} $$</p> <p>某个模型的上下文窗口为什么是某个值,通常由三层因素共同决定。第一层是<strong>训练时见过的位置范围</strong>:模型在预训练或后训练中通常只在某个最大长度内学习位置关系,这会体现在 <code>config.json</code> 的 <code>max_position_embeddings</code>、<code>max_model_len</code> 或类似字段里。第二层是<strong>位置编码机制</strong>:现代 LLM 多用 <a href="https://arxiv.org/abs/2104.09864" target="_blank" rel="noopener">RoPE</a> 这类相对位置编码,能通过 RoPE scaling、YaRN 等方法把原生窗口外推到更长范围,但外推越远,质量越需要实测验证。第三层是<strong>推理系统限制</strong>:即使模型理论上支持 128K,serving 框架也必须能分配足够 KV cache,并在可接受的 TTFT、ITL 和显存成本下跑起来。</p> <p>因此,model card 上的上下文窗口不是一个纯数学常数,而更像“训练设定 + 架构设计 + 工程验证”共同给出的服务承诺。比如一个模型原生训练到 32K,通过 YaRN 外推到 128K,含义是它在配置和验证上支持 128K;但如果线上请求几乎都是短 prompt,强行把 <code>max_model_len</code> 开到最大,反而可能浪费 KV cache 预算。</p> <p>例如 <a href="https://huggingface.co/Qwen/Qwen3.5-35B-A3B" target="_blank" rel="noopener"><code>Qwen3.5-35B-A3B</code></a> 的 model card 标注了 262,144 token 原生上下文,并可扩展到约 1,010,000 token。这个数字说明 runtime 可以配置很长的 <code>max_model_len</code>,但实际能否线上使用,还要看显存、TTFT、KV cache 和请求长度分布。</p> <p>可以把上下文窗口想成考试时允许带进考场的一摞资料。窗口越大,能带的资料越多;但资料越厚,读题(prefill)越慢,便签(KV cache)越占显存。长上下文服务的难点不是“模型宣称支持 128K”,而是在线上并发里让很多用户同时使用长上下文时仍然满足延迟和成本目标。</p> <p>“能放进上下文”也不等于“模型能稳定用好”。超长上下文可能出现检索位置偏置、注意力稀释、无关信息干扰等问题。因此工程上常配合上下文压缩:</p> <ul> <li><strong>摘要压缩</strong>:把历史对话或长文档先压成短摘要,牺牲细节换取空间。</li> <li><strong>RAG 检索</strong>:只把最相关片段放进 prompt,而不是把整库塞进去。</li> <li><strong>滑动窗口</strong>:保留最近若干轮对话,丢弃较早内容。</li> <li><strong>工具结果裁剪</strong>:对搜索、日志、网页结果做截断、去重、重排。</li> <li><strong>长上下文外推</strong>:用 RoPE scaling 等方法扩展位置编码,例如 Qwen3 文档中提到的 <a href="https://arxiv.org/abs/2309.00071" target="_blank" rel="noopener">YaRN</a>。</li> </ul> <p>上下文压缩和 KV cache 优化是两类不同问题:前者减少“要读的内容”,后者降低“读过内容的缓存成本”。例如 prefix cache 没有压缩语义信息,只是复用相同前缀的计算结果;RAG 则是真的改变了进入模型的文本。</p> <h2 id="2-模型文件结构">2. 模型文件结构</h2> <h3 id="21-模型目录里有哪些文件">2.1 模型目录里有哪些文件</h3> <p>一个 Hugging Face 风格的 causal LM 目录通常长这样:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>model/ </span></span><span style="display:flex;"><span> config.json </span></span><span style="display:flex;"><span> generation_config.json </span></span><span style="display:flex;"><span> tokenizer.json </span></span><span style="display:flex;"><span> tokenizer_config.json </span></span><span style="display:flex;"><span> special_tokens_map.json </span></span><span style="display:flex;"><span> model.safetensors </span></span><span style="display:flex;"><span> model-00001-of-00004.safetensors </span></span><span style="display:flex;"><span> model-00002-of-00004.safetensors </span></span><span style="display:flex;"><span> model.safetensors.index.json </span></span><span style="display:flex;"><span> README.md </span></span></code></pre></div><p>各类文件的职责可以这样理解:</p> <table> <thead> <tr> <th style="text-align: left">文件</th> <th style="text-align: left">作用</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code>config.json</code></td> <td style="text-align: left">描述模型结构,例如层数、hidden size、attention heads、context length</td> </tr> <tr> <td style="text-align: left"><code>*.safetensors</code> / <code>*.bin</code></td> <td style="text-align: left">真正的模型权重,也就是大量矩阵参数</td> </tr> <tr> <td style="text-align: left"><code>tokenizer.json</code></td> <td style="text-align: left">文本和 token id 之间的转换规则</td> </tr> <tr> <td style="text-align: left"><code>tokenizer_config.json</code></td> <td style="text-align: left">tokenizer 的附加配置,常包含 chat template</td> </tr> <tr> <td style="text-align: left"><code>generation_config.json</code></td> <td style="text-align: left">默认生成参数,例如 temperature、top_p、eos token</td> </tr> <tr> <td style="text-align: left"><code>README.md</code></td> <td style="text-align: left">model card,说明训练、用途、限制、license、示例</td> </tr> </tbody> </table> <p>可以把模型目录理解成一个可执行程序的“安装包”:权重是主体二进制,config 是硬件说明书,tokenizer 是输入法,model card 是用户手册。</p> <h3 id="22-权重文件是什么">2.2 权重文件是什么</h3> <p>权重文件存储的是神经网络中所有可学习参数。在线性层里,它们通常是矩阵;在 embedding 层里,它们像一张 token id 到向量的查表;在 attention 和 MLP 中,它们决定了模型如何把上下文变换成下一个 token 的 logits。</p> <p>logits 可以理解成模型给词表里每个候选 token 打的“未归一化分数”。例如 prompt 是“北京是中国的”,模型可能给 <code>首</code>、<code>省</code>、<code>城市</code> 等候选 token 分别打分;这些分数经过 softmax 才会变成概率,生成策略再根据概率选择下一个 token。因此,模型前向计算本身只是产出分数分布,真正输出哪个 token 还取决于 temperature、top-p、beam search 等解码策略。</p> <p>现代模型常用 <a href="https://huggingface.co/docs/safetensors/index" target="_blank" rel="noopener">safetensors</a> 格式,因为它避免了 Python pickle 反序列化的安全问题,并支持更高效的张量加载。大模型权重经常被切成多个 shard,例如 <code>model-00001-of-00008.safetensors</code>,原因很朴素:单个文件太大,不便于下载、校验和并行加载。</p> <p>一个容易混淆的点是:<strong>权重文件不是模型代码</strong>。真正执行前向计算的是 PyTorch、Transformers、vLLM、SGLang 或 llama.cpp 中的模型实现;权重文件只是把这些实现中的矩阵填上具体数值。也就是说,<code>config.json</code> 告诉 runtime“这是一栋 32 层楼的建筑”,权重文件告诉 runtime“每层楼具体摆了哪些钢筋水泥”。</p> <h3 id="23-tokenizer-是什么">2.3 tokenizer 是什么</h3> <p>tokenizer 负责把人类文本转换成模型能处理的整数序列。模型并不直接理解“北京”或“GPU”,它看到的是 token id,例如:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>文本:北京是中国的首都 </span></span><span style="display:flex;"><span>token:[&#34;北京&#34;, &#34;是&#34;, &#34;中国&#34;, &#34;的&#34;, &#34;首都&#34;] </span></span><span style="display:flex;"><span>id: [xxxxx, xxxx, xxxxx, xxx, xxxxx] </span></span></code></pre></div><p>实际切分不一定符合人类词语边界。英文中 <code>unbelievable</code> 可能被拆成 <code>un</code>、<code>believ</code>、<code>able</code>;中文中一个词也可能被拆成多个 token。tokenizer 的差异会直接影响:</p> <ul> <li>同一段文本算多少 token,从而影响上下文长度和计费。</li> <li>prompt 是否按模型训练时的格式组织。</li> <li>stop token、BOS/EOS、role token 是否正确。</li> </ul> <p>聊天模型还依赖 <a href="https://huggingface.co/docs/transformers/main/chat_templating" target="_blank" rel="noopener">chat template</a>。例如同样的消息:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>user: 你好 </span></span><span style="display:flex;"><span>assistant: 你好,有什么可以帮你? </span></span></code></pre></div><p>不同模型可能需要被格式化成 <code>[INST]...[/INST]</code>、<code>&lt;|user|&gt;...&lt;|assistant|&gt;</code> 或其他控制 token。如果 chat template 用错,模型就像看到一份格式混乱的试卷,能力会明显下降。</p> <h3 id="24-模型配置决定什么">2.4 模型配置决定什么</h3> <p><code>config.json</code> 决定 runtime 如何搭建模型结构。几个常见字段:</p> <table> <thead> <tr> <th style="text-align: left">配置</th> <th style="text-align: left">含义</th> <th style="text-align: left">影响</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code>num_hidden_layers</code></td> <td style="text-align: left">Transformer 层数</td> <td style="text-align: left">影响计算量和权重大小</td> </tr> <tr> <td style="text-align: left"><code>hidden_size</code></td> <td style="text-align: left">每个 token 的隐藏向量维度</td> <td style="text-align: left">影响矩阵乘法规模</td> </tr> <tr> <td style="text-align: left"><code>num_attention_heads</code></td> <td style="text-align: left">Query attention heads 数量</td> <td style="text-align: left">影响 attention 并行结构</td> </tr> <tr> <td style="text-align: left"><code>num_key_value_heads</code></td> <td style="text-align: left">KV heads 数量</td> <td style="text-align: left">影响 KV cache 大小,GQA/MQA 会减少它</td> </tr> <tr> <td style="text-align: left"><code>max_position_embeddings</code></td> <td style="text-align: left">原生上下文长度</td> <td style="text-align: left">影响最多能看多长输入</td> </tr> <tr> <td style="text-align: left"><code>rope_scaling</code></td> <td style="text-align: left">RoPE 外推配置</td> <td style="text-align: left">影响长上下文扩展</td> </tr> <tr> <td style="text-align: left"><code>torch_dtype</code></td> <td style="text-align: left">默认精度</td> <td style="text-align: left">影响显存占用和硬件兼容</td> </tr> </tbody> </table> <p>例如 <a href="https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct" target="_blank" rel="noopener"><code>Qwen2.5-Coder-7B-Instruct</code></a> 的 model card 会列出层数、attention heads、KV heads 和 context length。对 serving 工程来说,<code>num_key_value_heads</code> 特别关键:它决定每个 token 要缓存多少 K/V 向量,进而影响高并发长上下文时的显存压力。</p> <h2 id="3-模型本身架构">3. 模型本身架构</h2> <p>前面看到的 <code>config.json</code> 和权重文件,合在一起描述的正是一个神经网络。绝大多数文本 LLM 都是 <strong>decoder-only Transformer</strong>:token 先变成向量,依次穿过许多相似的 Transformer block,最后得到词表上每个候选 token 的 logits。模型“有多少层”“有多宽”“有多少个 attention/KV heads”“是否采用 MoE”,说的都是这条计算路径的不同设计。</p> <div class="mermaid">flowchart LR A["Token IDs"] --> B["Token embedding"] B --> C["Transformer block × N"] C --> D["Final norm"] D --> E["LM head"] E --> F["Logits over vocabulary"] subgraph Block["每个 Transformer block"] direction LR G["RMSNorm"] --> H["Causal self-attention"] --> I["Residual add"] --> J["RMSNorm"] --> K["MLP 或 MoE"] --> L["Residual add"] end </div> <p>图中的 block 会堆叠几十层甚至上百层。每层都保留一条 residual connection(残差连接),让新计算结果加回原来的隐藏状态;<code>RMSNorm</code>、LayerNorm 等归一化层则帮助数值保持稳定。不同模型的具体排列会有差异,例如 norm 放在子层前还是后、是否共享输入和输出 embedding,但不改变“attention 负责从上下文取信息,MLP/MoE 负责逐 token 变换”的主干。</p> <h3 id="31-causal-self-attention模型如何读取上文">3.1 Causal self-attention:模型如何读取上文</h3> <p>对某一层的每个 token 向量,attention 会分别线性投影出 Query(Q)、Key(K)和 Value(V)。直觉上,当前 token 用 Q 提出“我需要什么信息”,用它和历史 token 的 K 计算相关性,再按照权重汇总对应的 V:</p> <p>$$ \mathbf{Q} = \mathbf{X}\mathbf{W}_Q, \qquad \mathbf{K} = \mathbf{X}\mathbf{W}_K, \qquad \mathbf{V} = \mathbf{X}\mathbf{W}_V $$</p> <p>$$ \operatorname{Attention}(\mathbf{Q}, \mathbf{K}, \mathbf{V}) = \operatorname{softmax}\left(\frac{\mathbf{Q}\mathbf{K}^{\mathsf{T}}}{\sqrt{d_h}} + \mathbf{M}_{\mathrm{causal}}\right)\mathbf{V} $$</p> <p>其中,$\mathbf{X}$ 是输入隐藏状态,$d_h$ 是单个 head 的维度,$\mathbf{M}_{\mathrm{causal}}$ 是 causal mask:它把当前位置右侧的 attention score 置为负无穷,因此“北京是中国的”在预测“首”时只能读取前面的 token,不能偷看尚未生成的“都”。这就是 causal decoder 能自回归生成的原因。</p> <p>多头注意力(multi-head attention)会把隐藏维度切成多个 head,让不同 head 可以学习不同的关联模式;所有 head 的结果再合并并投影回隐藏维度。传统 MHA 为每个 Q head 都保留一组 K/V head。现代模型常使用 MQA 或 GQA:MQA 的许多 Q head 共享一组 K/V,GQA 则按组共享若干 K/V。后两者会明显减少 KV cache 的体积,因此对长上下文 serving 很重要;这也解释了为什么 <code>num_key_value_heads</code> 往往小于 <code>num_attention_heads</code>。</p> <p>注意力不是“把整段文本一次读懂”的魔法。每一层都只是在当前表示上做一次加权信息交换;层数、训练数据和参数共同决定这种交换最终能形成多强的语言与推理能力。从推理成本看,prefill 时需要处理 prompt 内 token 两两之间的注意力关系,decode 时每个新 token 则要查询不断增长的历史 K/V,这正是后文 Prefill、Decode 和 KV cache 优化的计算基础。</p> <h3 id="32-mlp每个-token-的特征变换">3.2 MLP:每个 token 的特征变换</h3> <p>attention 让 token 之间交换信息;attention 之后的 MLP(也叫 FFN,feed-forward network)主要对<strong>每个 token 自己的隐藏向量</strong>做非线性变换。一个简化的两层 MLP 可以写成:</p> <p>$$ \mathbf{h}^{\prime} = \mathbf{W}_{\mathrm{down}}\sigma\left(\mathbf{W}_{\mathrm{up}}\mathbf{h}\right) $$</p> <p>其中 <code>W_up</code> 通常先把 <code>hidden_size</code> 扩展到更大的 <code>intermediate_size</code>,激活函数(如 SiLU/GELU)引入非线性,<code>W_down</code> 再投影回原维度。很多现代模型使用 SwiGLU 等门控变体:它额外计算一条 gate 分支,选择性地放大或抑制特征。MLP 往往占模型参数中的很大比例,也往往是单层矩阵乘法计算的重要来源。</p> <p>因此可以把一个 dense Transformer block 粗略记成两步:先用 attention 问“上下文里哪些信息与我有关”,再用 MLP 问“结合这些信息后,应怎样更新这个 token 的表示”。MoE 的关键变化正发生在第二步:它不再让所有 token 经过同一个 MLP,而是让路由器为每个 token 选择少数几个专家 MLP。</p> <h3 id="33-moe-模型架构稀疏激活的专家网络">3.3 MoE 模型架构:稀疏激活的专家网络</h3> <p>MoE(Mixture of Experts)通常保留 attention 子层不变,把 dense MLP 替换为一组并列的 expert MLP,并新增一个 router(也称 gate)。router 根据当前 token 的隐藏状态,为所有专家打分,选出 top-k 个专家;token 只经过被选中的专家,最后按路由权重合并结果。</p> <p>对隐藏状态 $\mathbf{h}$,一个简化的 top-$k$ 路由过程可写成:</p> <p>$$ \mathbf{p} = \operatorname{softmax}(\mathbf{W}_{\mathrm{router}}\mathbf{h}) $$</p> <p>$$ \mathcal{S} = \operatorname{TopK}(\mathbf{p}, k) $$</p> <p>$$ \operatorname{MoE}(\mathbf{h}) = \sum_{i \in \mathcal{S}} \tilde{p}_i E_i(\mathbf{h}) $$</p> <p>这里 $E_i$ 是第 $i$ 个 expert MLP,$\tilde{p}_i$ 是在被选中专家上重新归一化后的路由权重。不同模型会在 router 打分、归一化和共享专家的处理上有所差异,但“只激活少数专家”的原则相同。</p> <div class="mermaid">flowchart LR A["Token hidden state"] --> B["Router / gate"] B -->|"top-k routing"| C["Expert 1 MLP"] B -->|"top-k routing"| D["Expert 2 MLP"] B -->|"top-k routing"| E["Expert N MLP"] C --> F["Weighted combine"] D --> F E --> F F --> G["Residual output"] </div> <p>这带来一个重要区分:总参数(total parameters)包含所有专家,而激活参数(activated parameters)只计算当前 token 实际经过的共享部分和 top-k 专家。例如“35B-A3B”可以理解为模型总共约 35B 参数,但单个 token 的一次前向计算只会激活约 3B 参数量级。它让模型能拥有更大的总容量,同时把每个 token 的计算量控制在接近较小 dense 模型的水平。</p> <p>但 MoE 并不是“35B 模型只需要 3B 的显存”。正常部署时,router 可能把不同 token 路由到任意专家,所以完整专家权重通常仍需在 GPU 或整个多卡集群中可访问;总参数仍决定权重存储、下载和冷启动成本。MoE 的优势主要是降低每个 token 的 FLOPs,不会等比例降低权重显存,也不会消除 KV cache 的开销。</p> <p>MoE serving 还多了几项工程特性:</p> <ul> <li><strong>负载均衡</strong>:若大量 token 都选择同一专家,该专家会成为热点;训练会加入负载均衡目标,runtime 也要尽量让批次中的路由高效执行。</li> <li><strong>capacity 与丢弃/回退</strong>:一个专家在一个 batch 内能处理的 token 数有限。超过 capacity 的 token 需要重新路由、跳过或采用实现定义的回退策略;不同模型和 runtime 的行为并不完全相同。</li> <li><strong>专家并行</strong>:专家可分布在不同 GPU 上。这样能分摊权重显存,却会引入 token dispatch 和 all-to-all 通信;GPU 拓扑与通信带宽会直接影响吞吐和延迟。</li> <li><strong>共享专家与细粒度专家</strong>:一些新模型会增加所有 token 都经过的 shared expert,或把 MLP 切成更多、更小的专家。它们属于具体架构选择,部署时应以 model card 和 runtime 支持矩阵为准。</li> </ul> <p>选择 MoE 模型时,除了看 total/activated parameters,还应确认 runtime 是否支持该模型的 router、专家数和并行方式,并在真实请求长度和并发下压测。短 batch、跨 GPU 通信较慢或专家路由高度不均衡时,MoE 的实际收益可能低于参数命名带来的直觉;反过来,在足够大的 batch 和合适的 expert parallel 配置下,它能提供很有吸引力的质量—计算量折中。</p> <h2 id="4-模型加载与运行">4. 模型加载与运行</h2> <h3 id="41-启动服务时发生了什么">4.1 启动服务时发生了什么</h3> <p>当执行类似 <code>vllm serve Qwen/Qwen2.5-Coder-7B-Instruct</code> 时,系统大致会做这些事:</p> <div class="mermaid">flowchart LR A["解析模型名和参数"] --> B["下载或定位本地权重"] B --> C["读取 config 与 tokenizer"] C --> D["构建模型结构"] D --> E["加载权重到 CPU/GPU"] E --> F["初始化 CUDA kernels 和 attention backend"] F --> G["预留 KV cache 与工作区显存"] G --> H["启动 HTTP server"] H --> I["等待请求"] </div> <p>这里有两个时间概念:<strong>冷启动时间</strong>和<strong>请求延迟</strong>。冷启动是服务从 0 到 ready 的时间,主要花在下载、磁盘读取、权重加载、kernel 初始化;请求延迟则是服务 ready 后处理每个请求的时间。一个 70B 模型冷启动可能需要数分钟,但不代表每个请求都要等数分钟。</p> <p>这里的 kernel 指的是 <strong>GPU kernel</strong>,不是 Linux/macOS 的 system kernel。按照 <a href="https://docs.nvidia.com/cuda/cuda-programming-guide/index.html" target="_blank" rel="noopener">CUDA Programming Guide</a> 的语境,kernel 是运行在 GPU 上的一段并行计算函数,例如矩阵乘法、attention、RMSNorm、采样前的 logits 处理等。推理框架会尽量使用高度优化的 kernel,减少 Python 调度和显存读写开销。</p> <p>模型加载失败的常见原因也和这些步骤对应:模型架构 runtime 不支持、权重格式不支持、显存不够、CUDA/driver/PyTorch 版本不匹配、gated model 没有 token 权限等。</p> <h3 id="42-显存主要被谁占用">4.2 显存主要被谁占用</h3> <p>LLM serving 的显存通常被四类东西占用:</p> <ol> <li><strong>模型权重</strong>:近似 <code>参数量 × 每参数字节数</code>。8B BF16 约 16 GB,8B INT4 约 4 GB 再加 scale 等元数据。</li> <li><strong>KV cache</strong>:随 batch size、上下文长度、生成长度线性增长,长上下文和高并发时可能超过权重本身。</li> <li><strong>activation 和 workspace</strong>:前向计算中的临时张量、CUDA graph、attention kernel 工作区等。</li> <li><strong>runtime buffer</strong>:调度器、通信 buffer、tokenizer/CPU buffer、多卡通信等开销。</li> </ol> <p>KV cache 的粗略估算公式是:</p> <p>$$ M_{\mathrm{KV}} \approx 2 \times L \times H_{\mathrm{KV}} \times d_h \times T \times B \times b $$</p> <p>其中 $L$ 是层数,$H_{\mathrm{KV}}$ 是 KV heads 数,$d_h$ 是每个 head 的维度,$T$ 是每个序列缓存的 token 数,$B$ 是 batch 内序列数,$b$ 是每个数值占用的字节数;前面的 $2$ 代表 K 和 V。这个公式解释了为什么“并发”和“长上下文”会迅速吃掉显存:如果每个请求都有 8K token,上下文中的每个 token 都要在每一层留下 K/V 记录;batch 从 1 变 16,KV cache 也近似放大 16 倍。</p> <p>可以把权重看成餐厅固定装修,KV cache 看成每桌客人正在使用的餐具和便签。装修开店时就占空间;客人越多、点单越长,桌面上的临时物品越多。</p> <h3 id="43-单卡和多卡怎么跑">4.3 单卡和多卡怎么跑</h3> <p>单卡部署最简单:整个模型权重、KV cache 和计算都在一张 GPU 上。多卡部署的目标是解决两个问题:<strong>单卡放不下</strong>和<strong>单卡算不快</strong>。</p> <p>常见并行方式如下:</p> <table> <thead> <tr> <th style="text-align: left">并行方式</th> <th style="text-align: left">做法</th> <th style="text-align: left">适合场景</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">Tensor Parallelism</td> <td style="text-align: left">把一个矩阵切到多张 GPU 上一起算</td> <td style="text-align: left">大模型单层太大,常见于 vLLM/SGLang</td> </tr> <tr> <td style="text-align: left">Pipeline Parallelism</td> <td style="text-align: left">把不同层放到不同 GPU 上,像流水线一样传递</td> <td style="text-align: left">模型层数多、跨节点部署</td> </tr> <tr> <td style="text-align: left">Data Parallelism</td> <td style="text-align: left">多个副本各自处理不同请求</td> <td style="text-align: left">单副本能放下,但需要更高 QPS</td> </tr> <tr> <td style="text-align: left">Expert Parallelism</td> <td style="text-align: left">MoE 模型中把专家分散到不同 GPU</td> <td style="text-align: left">DeepSeek-V3、Mixtral 等 MoE 模型</td> </tr> </tbody> </table> <p><a href="https://docs.vllm.ai/en/latest/serving/parallelism_scaling/" target="_blank" rel="noopener">vLLM parallelism 文档</a> 和 <a href="https://docs.sglang.io/" target="_blank" rel="noopener">SGLang 文档</a> 都覆盖了这些部署形态。实践中,单机多卡优先考虑 tensor parallel;如果模型已经能单卡放下而只是 QPS 不够,多个副本加负载均衡往往更简单。</p> <h3 id="44-推理硬件基础">4.4 推理硬件基础</h3> <p>CPU 也能跑 LLM,尤其配合 <a href="https://github.com/ggml-org/llama.cpp" target="_blank" rel="noopener">llama.cpp</a> 和 GGUF 量化模型时,本地实验很方便。但 CPU 的并行矩阵计算能力和内存带宽通常远弱于服务器 GPU,所以它更适合小模型、低并发、离线或端侧场景。GPU 推理的优势在于:大量矩阵乘法可以并行执行,显存带宽高,Tensor Core 能加速 FP16/BF16/FP8/INT8 等低精度计算。</p> <p>一个 GPU 可以粗略拆成几类部件:</p> <table> <thead> <tr> <th style="text-align: left">部件</th> <th style="text-align: left">作用</th> <th style="text-align: left">对 LLM 推理的影响</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">HBM/GDDR 显存</td> <td style="text-align: left">存权重、KV cache、临时张量</td> <td style="text-align: left">决定模型和并发能不能放下</td> </tr> <tr> <td style="text-align: left">显存带宽</td> <td style="text-align: left">权重和 KV cache 每秒能搬多少数据</td> <td style="text-align: left">decode 阶段经常卡在这里</td> </tr> <tr> <td style="text-align: left">SM</td> <td style="text-align: left">GPU 的主要计算单元</td> <td style="text-align: left">执行大量并行线程</td> </tr> <tr> <td style="text-align: left">Tensor Core</td> <td style="text-align: left">专门加速矩阵乘法</td> <td style="text-align: left">对 FP16/BF16/FP8/INT8 推理很关键</td> </tr> <tr> <td style="text-align: left">NVLink/PCIe</td> <td style="text-align: left">GPU 间或 CPU-GPU 间通信</td> <td style="text-align: left">影响多卡 tensor parallel 和 KV 传输</td> </tr> </tbody> </table> <p>实际运行时,CPU 负责发起 kernel launch,GPU 在大量 SM 上并行执行 kernel。矩阵乘法会被切成许多 tile,Tensor Core 对这些小块做高吞吐乘加;attention kernel 则要在 Q/K/V、mask、softmax、输出投影之间组织计算和显存访问。很多推理优化的本质,不是“换一个数学公式”,而是减少不必要的数据搬运、kernel launch 和中间张量写回。</p> <p>常见硬件可以按定位理解:</p> <table> <thead> <tr> <th style="text-align: left">GPU</th> <th style="text-align: left">常见定位</th> <th style="text-align: left">直观理解</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">RTX 4090/5090</td> <td style="text-align: left">消费级开发、单机实验</td> <td style="text-align: left">便宜但显存和多卡互联受限</td> </tr> <tr> <td style="text-align: left">L20</td> <td style="text-align: left">国内云上常见推理卡</td> <td style="text-align: left">适合中小模型和成本敏感推理</td> </tr> <tr> <td style="text-align: left"><a href="https://www.nvidia.com/en-us/data-center/l40s/" target="_blank" rel="noopener">L40S</a></td> <td style="text-align: left">数据中心推理、图形和多模态</td> <td style="text-align: left">48GB 显存,兼顾 AI 与图形负载</td> </tr> <tr> <td style="text-align: left">A100</td> <td style="text-align: left">上一代训练和推理主力</td> <td style="text-align: left">40/80GB 显存,生态成熟</td> </tr> <tr> <td style="text-align: left"><a href="https://www.nvidia.com/en-us/data-center/h100/" target="_blank" rel="noopener">H100</a> / H200</td> <td style="text-align: left">高端训练和推理</td> <td style="text-align: left">HBM 带宽高,FP8 和 Transformer Engine 友好</td> </tr> <tr> <td style="text-align: left"><a href="https://www.nvidia.com/en-us/data-center/gb200-nvl72/" target="_blank" rel="noopener">B200/GB200</a></td> <td style="text-align: left">Blackwell 代大规模 AI 集群</td> <td style="text-align: left">面向更大模型、更高吞吐和更低 token 成本</td> </tr> </tbody> </table> <p>对 LLM serving 来说,GPU 型号不能只看 FLOPS。短 prompt 大 batch 的 prefill 更吃算力,长输出 decode 更吃显存带宽,长上下文高并发更吃显存容量和 KV cache 管理。选择硬件时,应同时看显存容量、显存带宽、低精度支持、互联方式和实际租赁成本。</p> <h2 id="5-请求处理流程prefill-与-decode">5. 请求处理流程:Prefill 与 Decode</h2> <h3 id="51-请求进入服务后经历哪些阶段">5.1 请求进入服务后经历哪些阶段</h3> <p>先看一个常见 LLM serving 架构。不同框架命名不同,但组件职责大体类似:</p> <div class="mermaid">flowchart LR C["Client / SDK"] --> A["API Server"] A --> T["Tokenizer / Chat Template"] T --> Q["Request Queue"] Q --> S["Scheduler"] S --> W["Model Worker / GPU Runtime"] W --> M["KV Cache Manager"] W --> D["Detokenizer / Streamer"] D --> A A --> C W --> O["Metrics / Logs / Traces"] S --> O </div> <p>其中 API Server 负责 HTTP、鉴权、OpenAI-compatible API 和流式返回;Tokenizer 把消息格式化并转成 token id;Request Queue 暂存待处理请求;Scheduler 决定哪些请求在下一次 GPU forward 中被合并;Model Worker/GPU Runtime 真正执行模型前向计算;KV Cache Manager 管理每个请求的历史 K/V;Detokenizer 把输出 token 转回文本;Metrics/Logs/Traces 则用于压测和线上诊断。</p> <p>一次在线请求从客户端到模型输出,通常经历:</p> <div class="mermaid">sequenceDiagram participant C as Client participant S as API Server participant Q as Scheduler participant G as GPU Runtime C->>S: HTTP chat request S->>S: apply chat template S->>S: tokenize S->>Q: enqueue request Q->>G: schedule prefill G-->>S: first token S-->>C: stream first token loop decode Q->>G: schedule next decode step G-->>S: next token S-->>C: stream token end S-->>C: finish reason / usage </div> <p>注意,客户端感受到的延迟不只是 GPU 计算时间,还包括网络、HTTP 处理、tokenization、排队、detokenization 和流式传输。压测时如果只看 GPU kernel 时间,会低估真实服务延迟。</p> <h3 id="52-prefill-阶段要理解什么">5.2 Prefill 阶段要理解什么</h3> <p>Prefill 是模型“读题”的阶段:服务把完整 prompt 一次性送入模型,计算所有输入 token 的 hidden states,并为后续生成建立 KV cache。因为 prompt 中的 token 已经全部知道,prefill 可以高度并行,所以它通常更偏 compute-bound。</p> <p>例子:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>prompt: 请总结下面这份 4000 token 的报告... </span></span><span style="display:flex;"><span>prefill: 一次性处理这 4000 个输入 token,建立 KV cache,并产生第一个输出 token </span></span></code></pre></div><p>Prefill 直接决定 TTFT(Time To First Token)的下限。prompt 越长,模型“读题”越久;如果前面还有其他请求排队,TTFT 还会继续上升。</p> <p>一些系统优化也发生在 prefill 附近。例如 vLLM 的 <a href="https://docs.vllm.ai/en/latest/features/automatic_prefix_caching/" target="_blank" rel="noopener">Automatic Prefix Caching</a> 会复用相同前缀的 KV cache;这对 RAG、长系统 prompt、多轮对话很有价值。类比一下:如果每个学生都交同一份试卷前半部分,老师没必要每次重新读一遍公共题干。</p> <h3 id="53-decode-阶段要理解什么">5.3 Decode 阶段要理解什么</h3> <p>Decode 是模型“逐字答题”的阶段。自回归模型必须等上一个 token 出来后,才能把它追加到上下文里预测下一个 token,因此 decode 在单条序列内部是串行的。</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>第 1 步:prompt -&gt; 北 </span></span><span style="display:flex;"><span>第 2 步:prompt + 北 -&gt; 京 </span></span><span style="display:flex;"><span>第 3 步:prompt + 北京 -&gt; 是 </span></span></code></pre></div><p>Decode 阶段有两个关键事实:</p> <ul> <li>每生成一个 token 都要读一遍模型权重,所以它常常 memory-bound。</li> <li>KV cache 避免了重复计算历史 token,但每步仍要读取历史 K/V 来做 attention。</li> </ul> <p>为什么 batching 能提升吞吐?假设服务同时有 16 个请求都在 decode。GPU 读取一次权重,可以顺便为 16 个请求各生成一个 token。单个请求未必更快,但系统总输出 token/s 会变高。</p> <p>这也是 latency-throughput trade-off 的来源:batch 大,GPU 利用率高,吞吐好;但请求可能等 batch、等调度、和其他请求共享资源,单请求延迟变差。</p> <h3 id="54-生成策略属于哪里">5.4 生成策略属于哪里</h3> <p>模型前向计算输出的是 logits,也就是每个 token 的分数。生成策略负责把分数变成真正输出的 token。</p> <p>常见参数:</p> <table> <thead> <tr> <th style="text-align: left">参数</th> <th style="text-align: left">作用</th> <th style="text-align: left">直观理解</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code>temperature</code></td> <td style="text-align: left">调整分布尖锐程度</td> <td style="text-align: left">越低越保守,越高越发散</td> </tr> <tr> <td style="text-align: left"><a href="https://arxiv.org/abs/1904.09751" target="_blank" rel="noopener"><code>top_p</code></a></td> <td style="text-align: left">只在累计概率前 p 的 token 中采样</td> <td style="text-align: left">从“高概率候选集合”里抽</td> </tr> <tr> <td style="text-align: left"><code>top_k</code></td> <td style="text-align: left">只保留概率最高的 k 个 token</td> <td style="text-align: left">限制候选菜单大小</td> </tr> <tr> <td style="text-align: left"><code>max_tokens</code></td> <td style="text-align: left">最多生成多少 token</td> <td style="text-align: left">防止输出无限变长</td> </tr> <tr> <td style="text-align: left"><code>stop</code></td> <td style="text-align: left">遇到指定字符串/token 停止</td> <td style="text-align: left">例如遇到 <code>&lt;/answer&gt;</code> 停止</td> </tr> <tr> <td style="text-align: left"><code>repetition_penalty</code></td> <td style="text-align: left">惩罚重复 token</td> <td style="text-align: left">减少复读</td> </tr> </tbody> </table> <p>如果 <code>temperature=0</code> 或接近 0,模型更像做选择题,总选最高分答案;如果 temperature 较高,则更像写作时保留更多表达可能。<a href="https://huggingface.co/docs/transformers/generation_strategies#beam-search" target="_blank" rel="noopener">Beam search</a> 会在每一步保留多个候选序列,适合翻译、图像描述这类输入约束较强的任务,但开放式聊天里可能更容易产生保守、重复的回答。工程上,生成参数不仅影响质量,也影响性能:<code>max_tokens</code> 越大,最坏 E2E latency 越高;beam search、parallel sampling 会增加每个请求的计算和 KV cache 压力。</p> <p>什么时候停止生成也很重要。最常见的停止条件有四种:</p> <ul> <li>模型生成 EOS token。EOS 是 End Of Sequence,表示模型认为序列结束。</li> <li>达到 <code>max_tokens</code> 或 <code>max_new_tokens</code> 上限。</li> <li>命中用户配置的 stop string,例如生成到 <code>&lt;/answer&gt;</code>。</li> <li>客户端取消、服务端 timeout,或调度器因资源压力中断请求。</li> </ul> <p>EOS 是 tokenizer 词表里的特殊 token,stop string 则是服务层按文本匹配的停止规则。<a href="https://huggingface.co/docs/transformers/main_classes/text_generation#transformers.GenerationConfig" target="_blank" rel="noopener">GenerationConfig</a> 里常见的 <code>eos_token_id</code>、<code>forced_eos_token_id</code>、<code>stop_strings</code> 就对应这些控制。二者经常一起用:EOS 让模型自然结束,stop string 用来适配业务协议或结构化输出。</p> <p>还有一类优化叫 <a href="https://arxiv.org/abs/2211.17192" target="_blank" rel="noopener">speculative decoding</a>:让小模型先草拟多个 token,大模型一次性验证。类比写论文时让助研先打草稿,导师只负责批改和确认。如果草稿命中率高,decode 可以明显加速。</p> <h3 id="55-什么是-pd-分离">5.5 什么是 PD 分离</h3> <p>PD 分离(prefill-decode disaggregation)指把 prefill 和 decode 放到不同 worker、不同 GPU,甚至不同机器上执行。动机很直接:prefill 更偏 compute-bound,decode 更偏 memory-bound;把它们混在同一批 GPU 上时,长 prompt 的 prefill 可能打断正在 decode 的请求,拉高 ITL,而 decode 的高频小步计算也会影响 prefill 调度。</p> <p>可以把它想成餐厅里把“读菜单和备菜”和“逐道上菜”拆成两个工位。前者适合大块时间集中处理,后者需要稳定、频繁、低抖动地出菜。拆开后,每个工位能按自己的瓶颈配置资源。</p> <p>研究系统 <a href="https://arxiv.org/abs/2311.18677" target="_blank" rel="noopener">Splitwise</a> 和 <a href="https://arxiv.org/abs/2401.09670" target="_blank" rel="noopener">DistServe</a> 都讨论了这种 phase splitting / disaggregation 思路。收益是降低 prefill 和 decode 的相互干扰,并可分别优化 TTFT 和 TPOT;代价是系统复杂度更高,需要在阶段之间传输 KV cache 或中间状态,对网络、调度器和容错都有更高要求。小规模单机部署通常不必一开始就做 PD 分离,但长上下文、高并发、严格 SLO 的生产集群会越来越关注它。</p> <h2 id="6-serving-框架对比">6. Serving 框架对比</h2> <h3 id="61-vllm">6.1 vLLM</h3> <p><a href="https://github.com/vllm-project/vllm" target="_blank" rel="noopener">vLLM</a> 的核心目标是 GPU 服务端高吞吐推理。它最出名的设计是 <a href="https://arxiv.org/abs/2309.06180" target="_blank" rel="noopener">PagedAttention</a>:借鉴操作系统虚拟内存分页思想,把 KV cache 分块管理,减少连续大块显存分配带来的碎片和浪费。</p> <p>直观地说,传统 KV cache 管理像给每个请求预留一整排连续座位;如果客人中途离开或增加,座位容易碎片化。PagedAttention 像把座位拆成小块,按需分配、可复用、可共享,从而容纳更多并发请求。</p> <p>vLLM 适合:</p> <ul> <li>部署 Hugging Face 上的主流 open-weight 模型。</li> <li>提供 OpenAI-compatible API。</li> <li>追求较高吞吐和较成熟的生产部署能力。</li> <li>需要量化、多卡、prefix caching、benchmark 工具链。</li> </ul> <p>它不只是“模型 runner”,而是一个面向 serving 的系统:调度、batching、KV cache 管理、metrics、并行策略都很重要。学习 vLLM,重点不是背启动命令,而是理解它为什么能在同等显存下塞进更多有效请求。</p> <h3 id="62-ollama">6.2 Ollama</h3> <p><a href="https://github.com/ollama/ollama" target="_blank" rel="noopener">Ollama</a> 更像“本地模型运行器 + 模型管理工具”。它降低了本地跑模型的门槛:<code>ollama run llama3.1</code> 就能拉模型、启动 runtime、进入对话。它默认提供本地 API,官方文档中默认 base URL 是 <code>http://localhost:11434/api</code>,并支持 <a href="https://docs.ollama.com/openai" target="_blank" rel="noopener">OpenAI compatibility</a>。</p> <p>Ollama 的一个重要概念是 <a href="https://docs.ollama.com/modelfile" target="_blank" rel="noopener">Modelfile</a>,类似 Dockerfile:用 <code>FROM</code> 指定基础模型,用 <code>SYSTEM</code> 写系统提示词,用 <code>PARAMETER</code> 固定 temperature、context length 等运行参数。</p> <p>它适合:</p> <ul> <li>个人电脑、本地开发、离线演示。</li> <li>快速比较不同模型。</li> <li>把 GGUF 模型包装成简单 API。</li> <li>对极致吞吐要求不高,但希望易用。</li> </ul> <p>Ollama 通常不是大规模线上 GPU serving 的首选,但非常适合建立直觉:模型如何被拉取、加载、运行、暴露 API,以及量化模型在本地硬件上如何权衡速度和质量。</p> <h3 id="63-sglang">6.3 SGLang</h3> <p><a href="https://github.com/sgl-project/sglang" target="_blank" rel="noopener">SGLang</a> 的定位是高性能 serving 和复杂 LLM program 执行。它不只关心“单次 chat completion”,还关心多轮调用、结构化输出、分支控制、工具调用、agent workflow 等更复杂的推理程序。</p> <p>其论文 <a href="https://arxiv.org/abs/2312.07104" target="_blank" rel="noopener">SGLang: Efficient Execution of Structured Language Model Programs</a> 提出了前端语言和后端 runtime 的组合;后端中的 RadixAttention 用于复用共享前缀的 KV cache。对多轮对话、few-shot prompt、RAG 模板这类“很多请求共享长前缀”的工作负载,这类优化很关键。</p> <p>可以把 SGLang 想成“懂得复用草稿纸的考试系统”:如果 100 道题都共享同一段材料,它不会让模型每次从头读材料,而是尽量复用已经算过的前缀状态。</p> <p>它适合:</p> <ul> <li>结构化输出,如 JSON、regex、schema-constrained generation。</li> <li>agent、多步推理、复杂 prompt program。</li> <li>高吞吐、低延迟、多卡 serving。</li> <li>需要快速跟进新模型和 reasoning parser 的场景。</li> </ul> <h3 id="64-其他相关框架">6.4 其他相关框架</h3> <p>除了 vLLM、Ollama、SGLang,还常见以下系统:</p> <ul> <li><a href="https://github.com/ggml-org/llama.cpp" target="_blank" rel="noopener">llama.cpp</a>:C/C++ 实现,主打 GGUF、CPU/Metal/CUDA/端侧推理,是很多本地工具的底座。</li> <li><a href="https://nvidia.github.io/TensorRT-LLM/" target="_blank" rel="noopener">TensorRT-LLM</a>:NVIDIA 面向高性能推理的优化库,适合愿意为特定硬件做更深优化的生产场景。</li> <li><a href="https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/index.html" target="_blank" rel="noopener">Triton Inference Server</a>:通用推理服务框架,可服务多种模型后端,不只 LLM。</li> <li><a href="https://github.com/InternLM/lmdeploy" target="_blank" rel="noopener">LMDeploy</a>:国内常见的 LLM 部署工具链,支持 TurboMind、量化和服务化。</li> <li><a href="https://github.com/huggingface/text-generation-inference" target="_blank" rel="noopener">Text Generation Inference</a>:Hugging Face 早期生产 serving 代表,现在更适合作为历史和架构参考。</li> </ul> <p>选型时可以先问:我要的是本地易用、服务端吞吐、复杂程序执行、硬件极致优化,还是通用推理平台?不同答案会指向不同工具。</p> <h2 id="7-并发batch-与调度">7. 并发、Batch 与调度</h2> <h3 id="71-什么是并发">7.1 什么是并发</h3> <p>“并发”在 LLM serving 里至少有四种口径:</p> <table> <thead> <tr> <th style="text-align: left">口径</th> <th style="text-align: left">含义</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">客户端并发</td> <td style="text-align: left">同时有多少客户端连接或请求在发起</td> </tr> <tr> <td style="text-align: left">排队并发</td> <td style="text-align: left">有多少请求已经进入系统但还没完成</td> </tr> <tr> <td style="text-align: left">running sequences</td> <td style="text-align: left">有多少序列正在被调度器处理</td> </tr> <tr> <td style="text-align: left">batch size / batched tokens</td> <td style="text-align: left">某次 GPU forward 实际合并了多少序列或 token</td> </tr> </tbody> </table> <p>用户说“并发 100”,可能只是压测工具同时开 100 个连接;但 GPU 看到的可能是某一时刻 batch 里有 32 条 decode 序列,另有 68 条排队或等待 prefill。分析问题时必须先明确口径。</p> <p>一个小例子:咖啡店门口有 100 人排队,不代表咖啡机同时在做 100 杯。真正同时制作的杯数取决于咖啡机容量、店员调度、每杯饮品复杂度和是否有人点超大杯。</p> <h3 id="72-并发和吞吐是什么关系">7.2 并发和吞吐是什么关系</h3> <p>并发升高通常会先提升吞吐,因为 GPU 从“小批量低利用率”变成“更大 batch、更高利用率”。但超过某个点后,吞吐会趋于饱和,延迟开始快速上升。</p> <p>可以用一个简化关系理解:</p> <p>$$ \overline{L} = \lambda,\overline{W} $$</p> <p>这就是 <a href="https://en.wikipedia.org/wiki/Little%27s_law" target="_blank" rel="noopener">Little&rsquo;s Law</a> 在服务系统中的直觉版本:$\overline{L}$ 是系统内平均未完成请求数,$\lambda$ 是稳定到达率(例如 QPS),$\overline{W}$ 是平均端到端延迟。假设服务稳定处理 10 QPS,每个请求平均 5 秒完成,那么系统里平均大约有 50 个请求处于未完成状态。</p> <p>LLM 的特殊性在于,不同请求的“大小”差异很大:一个 50 token 输入、20 token 输出的请求,和一个 8000 token 输入、2000 token 输出的请求,对 GPU 的占用完全不是一个量级。因此只说 QPS 不够,必须同时说 input/output token 分布。</p> <h3 id="73-serving-调度器要解决什么">7.3 Serving 调度器要解决什么</h3> <p>LLM serving 调度器的任务是把请求组织成 GPU 能高效处理的批次,同时尽量控制延迟。它要处理几类矛盾:</p> <ul> <li>prefill 计算重,decode 更频繁但每步较小。</li> <li>新请求希望尽快 prefill 以降低 TTFT,老请求希望 decode 连续进行以降低 ITL。</li> <li>batch 大有利于吞吐,但会增加等待和干扰。</li> <li>长请求占用 KV cache 久,短请求又不应该被饿死。</li> </ul> <p>continuous batching 的思想是:不要等一整个固定 batch 全部完成后再接新请求,而是在每个 decode step 动态加入新请求、移除完成请求。这比传统静态 batching 更适合 LLM,因为每个请求输出长度不同。</p> <p>chunked prefill 则是把很长的 prefill 切成块,避免一个超长 prompt 长时间占住 GPU,导致正在 decode 的请求 token 间隔变大。vLLM 的 <a href="https://docs.vllm.ai/en/latest/configuration/optimization/" target="_blank" rel="noopener">optimization 文档</a> 中就把 chunked prefill 作为调优点之一。</p> <h3 id="74-什么是容量规划">7.4 什么是容量规划</h3> <p>容量规划回答的问题是:在给定模型、硬件和 SLO 下,服务最多能承载多少负载。SLO 是 Service Level Objective,即服务质量目标,例如 <code>P99 TTFT &lt; 1s</code>、<code>P99 ITL &lt; 80ms</code>、错误率低于某个阈值。它不是“系统极限能跑多快”,而是“在用户体验可接受的条件下能跑多大负载”。</p> <p>正确的压测方式不是只问“最大 QPS 是多少”,而是固定场景:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>模型:Qwen2.5-Coder-7B-Instruct </span></span><span style="display:flex;"><span>硬件:1 × A100 80GB </span></span><span style="display:flex;"><span>输入长度分布:P50 1K,P95 8K </span></span><span style="display:flex;"><span>输出长度分布:P50 256,P95 1K </span></span><span style="display:flex;"><span>SLO:P99 TTFT &lt; 1s,P99 ITL &lt; 80ms </span></span><span style="display:flex;"><span>目标:满足 SLO 下的最大 goodput </span></span></code></pre></div><p>如果没有这些约束,QPS 数字几乎不可比较。短请求 benchmark 可以把 QPS 做得很好看,但一上线到长上下文 RAG,就会因为 prefill 和 KV cache 压力完全不同而失真。</p> <p>工程上可以分三类压测:短问答压测看 API overhead 和 decode;长 prompt 压测看 prefill 和 prefix cache;长输出压测看 decode 吞吐和显存稳定性。</p> <h2 id="8-可观测性-observability">8. 可观测性 Observability</h2> <p>如果说前几节是在解释“LLM serving 如何工作”,那么可观测性(observability)关心的是:<strong>服务运行时到底发生了什么,为什么变慢,哪里快要违反 SLO,改动之后是否真的变好</strong>。</p> <p>在云原生系统里,可观测性通常围绕 metrics、logs、traces 展开;LLM serving 也一样,但会多出一组模型相关信号:输入/输出 token 长度、TTFT、ITL、KV cache、batch、prefix cache、GPU 显存、调度队列、goodput 等。它不是简单做一个 GPU 利用率面板,而是要把“用户请求”“模型阶段”“调度器状态”“硬件资源”连起来看。</p> <h3 id="81-观测对象与信号">8.1 观测对象与信号</h3> <p>一个 LLM 服务可以按层观测:</p> <div class="mermaid">flowchart LR C["Client"] --> A["API Server"] A --> Q["Queue / Scheduler"] Q --> W["Model Worker"] W --> K["KV Cache"] W --> G["GPU"] A -. logs/traces .-> O["Observability Backend"] Q -. metrics .-> O W -. metrics/traces .-> O G -. GPU metrics .-> O </div> <p>常见观测信号如下:</p> <table> <thead> <tr> <th style="text-align: left">信号</th> <th style="text-align: left">记录什么</th> <th style="text-align: left">典型用途</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">Metrics</td> <td style="text-align: left">数值型时间序列</td> <td style="text-align: left">看趋势、报警、SLO、容量规划</td> </tr> <tr> <td style="text-align: left">Logs</td> <td style="text-align: left">离散事件和错误上下文</td> <td style="text-align: left">查失败请求、OOM、超时、异常栈</td> </tr> <tr> <td style="text-align: left">Traces</td> <td style="text-align: left">一次请求跨组件的阶段耗时</td> <td style="text-align: left">定位慢在排队、prefill、decode 还是网络</td> </tr> <tr> <td style="text-align: left">Profiles</td> <td style="text-align: left">kernel、CPU、内存等细粒度性能剖析</td> <td style="text-align: left">优化热点、排查低 GPU 利用率</td> </tr> <tr> <td style="text-align: left">Benchmarks</td> <td style="text-align: left">受控负载下的实验结果</td> <td style="text-align: left">比较模型、框架、参数和硬件</td> </tr> </tbody> </table> <p>一个简单类比:metrics 像体检指标,logs 像病历,traces 像从挂号到检查到开药的全流程时间线,benchmarks 则像标准化体能测试。</p> <h3 id="82-延迟指标">8.2 延迟指标</h3> <p>LLM serving 的延迟指标要按阶段拆:</p> <table> <thead> <tr> <th style="text-align: left">指标</th> <th style="text-align: left">含义</th> <th style="text-align: left">主要对应阶段</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">TTFT</td> <td style="text-align: left">Time To First Token,请求发出到第一个 token 返回</td> <td style="text-align: left">排队 + tokenization + prefill</td> </tr> <tr> <td style="text-align: left">ITL / TPOT</td> <td style="text-align: left">Inter-Token Latency / Time Per Output Token,相邻输出 token 间隔</td> <td style="text-align: left">decode</td> </tr> <tr> <td style="text-align: left">E2E latency</td> <td style="text-align: left">End-to-End latency,完整响应耗时</td> <td style="text-align: left">全流程</td> </tr> <tr> <td style="text-align: left">queue time</td> <td style="text-align: left">请求在调度器里等待的时间</td> <td style="text-align: left">调度</td> </tr> <tr> <td style="text-align: left">prefill latency</td> <td style="text-align: left">处理输入 prompt 的时间</td> <td style="text-align: left">prefill</td> </tr> <tr> <td style="text-align: left">decode latency</td> <td style="text-align: left">生成输出 token 的累计时间</td> <td style="text-align: left">decode</td> </tr> </tbody> </table> <p>一个简化公式:</p> <p>$$ T_{\mathrm{E2E}} \approx T_{\mathrm{TTFT}} + \bigl(N_{\mathrm{output}} - 1\bigr) T_{\mathrm{TPOT}} $$</p> <p>如果输出很短,TTFT 主导体验;如果输出很长,TPOT 主导体验。流式聊天中,用户最先感知的是 TTFT,随后感知的是 token “蹦出来”的速度,也就是 ITL/TPOT。非流式接口则会把全部内容攒到最后返回,用户只感受到 E2E latency,但服务端仍然应该记录 TTFT 和 ITL,否则很难判断慢在 prefill 还是 decode。</p> <h3 id="83-吞吐与资源指标">8.3 吞吐与资源指标</h3> <p>吞吐有多个口径:</p> <table> <thead> <tr> <th style="text-align: left">指标</th> <th style="text-align: left">单位</th> <th style="text-align: left">解释</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">QPS / RPS</td> <td style="text-align: left">req/s</td> <td style="text-align: left">每秒完成多少请求</td> </tr> <tr> <td style="text-align: left">input throughput</td> <td style="text-align: left">input tok/s</td> <td style="text-align: left">每秒处理多少输入 token</td> </tr> <tr> <td style="text-align: left">output throughput</td> <td style="text-align: left">output tok/s</td> <td style="text-align: left">每秒生成多少输出 token</td> </tr> <tr> <td style="text-align: left">total token throughput</td> <td style="text-align: left">tok/s</td> <td style="text-align: left">输入和输出 token 总量</td> </tr> <tr> <td style="text-align: left">per-GPU throughput</td> <td style="text-align: left">tok/s/GPU</td> <td style="text-align: left">多卡场景下的资源效率</td> </tr> </tbody> </table> <p>对 LLM 来说,单看 QPS 很危险。假设两个服务都是 10 QPS:服务 A 每个请求输出 20 token,服务 B 每个请求输出 1000 token,它们的 GPU 压力完全不同。更稳妥的表达是:“在某个输入/输出长度分布下,满足某个延迟 SLO 时,每张 GPU 每秒能产出多少 output tokens。”</p> <p>还要同时看资源和调度指标:</p> <ul> <li>GPU utilization、显存使用量、显存带宽、功耗、温度。</li> <li>KV cache 使用量、block 使用率、prefix cache hit rate。</li> <li>scheduler queue length、running requests、waiting requests。</li> <li>preemption / eviction 次数。</li> <li>HTTP 4xx/5xx、timeout、cancel、OOM。</li> <li>cost per 1M tokens。</li> </ul> <p><a href="https://docs.vllm.ai/en/latest/usage/metrics/" target="_blank" rel="noopener">vLLM Production Metrics</a> 就暴露了 generation tokens、prefix cache hits、preemptions、speculative decoding、KV connector 等指标;这些指标比单纯的 GPU utilization 更接近 LLM serving 的真实瓶颈。</p> <h3 id="84-分位数-slo-与-goodput">8.4 分位数 SLO 与 Goodput</h3> <p>线上服务不要只看平均值。平均 TTFT 可能是 300ms,但 P99 TTFT 是 5s,用户投诉通常来自后者。</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>P50 TTFT = 300ms:一半请求首 token 延迟小于 300ms </span></span><span style="display:flex;"><span>P90 TTFT = 900ms:90% 请求小于 900ms </span></span><span style="display:flex;"><span>P99 TTFT = 5s:99% 请求小于 5s,最慢 1% 更糟 </span></span></code></pre></div><p>LLM 服务的长尾来自很多地方:个别超长 prompt、调度器排队、KV cache preemption、GPU kernel 抖动、网络慢客户端、模型冷加载、输出长度差异。博士生做实验时尤其要注意:只报告 mean latency 往往不够,至少应报告 P50/P90/P99 和长度分布。</p> <p>Goodput 指的是满足 SLO 的有效吞吐。例如系统每秒完成 100 个请求,但只有 70 个请求满足 <code>TTFT &lt; 1s</code> 且 <code>TPOT &lt; 80ms</code>,那么 goodput 是 70 req/s,而不是 100 req/s。</p> <p>一个常见误区是追求最高 output tok/s。极限吞吐通常靠大 batch 换来,可能显著牺牲 TTFT 和 P99;真正上线时,要在 SLO 约束下比较吞吐。</p> <h3 id="85-日志与-trace">8.5 日志与 Trace</h3> <p>Metrics 适合看趋势,但一次具体请求为什么慢,通常要靠日志和 trace。建议至少记录这些字段:</p> <table> <thead> <tr> <th style="text-align: left">字段</th> <th style="text-align: left">例子</th> <th style="text-align: left">用途</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">request id</td> <td style="text-align: left"><code>req-123</code></td> <td style="text-align: left">串联 API server、scheduler、worker 日志</td> </tr> <tr> <td style="text-align: left">model</td> <td style="text-align: left"><code>Qwen2.5-Coder-7B-Instruct</code></td> <td style="text-align: left">多模型服务定位问题</td> </tr> <tr> <td style="text-align: left">input/output tokens</td> <td style="text-align: left"><code>1024 / 256</code></td> <td style="text-align: left">解释延迟和成本</td> </tr> <tr> <td style="text-align: left">queue / prefill / decode time</td> <td style="text-align: left"><code>20ms / 300ms / 4s</code></td> <td style="text-align: left">阶段归因</td> </tr> <tr> <td style="text-align: left">finish reason</td> <td style="text-align: left"><code>eos / length / stop / error</code></td> <td style="text-align: left">判断是否异常终止</td> </tr> <tr> <td style="text-align: left">error code</td> <td style="text-align: left"><code>timeout / oom / cancelled</code></td> <td style="text-align: left">失败分类</td> </tr> </tbody> </table> <p>Trace 则把一次请求拆成多个 span,例如:</p> <div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>HTTP receive </span></span><span style="display:flex;"><span> -&gt; auth / routing </span></span><span style="display:flex;"><span> -&gt; apply chat template </span></span><span style="display:flex;"><span> -&gt; tokenize </span></span><span style="display:flex;"><span> -&gt; queue wait </span></span><span style="display:flex;"><span> -&gt; prefill </span></span><span style="display:flex;"><span> -&gt; decode step loop </span></span><span style="display:flex;"><span> -&gt; detokenize </span></span><span style="display:flex;"><span> -&gt; stream response </span></span></code></pre></div><p><a href="https://opentelemetry.io/docs/what-is-opentelemetry/" target="_blank" rel="noopener">OpenTelemetry</a> 是云原生里常用的开放标准,用来生成、收集和导出 traces、metrics、logs。对 LLM 应用层,还可以使用 <a href="https://langfuse.com/docs" target="_blank" rel="noopener">Langfuse</a> 或 <a href="https://github.com/traceloop/openllmetry" target="_blank" rel="noopener">OpenLLMetry</a> 这类工具观测 RAG、agent、tool call、prompt 版本、用户反馈和成本。它们和 serving runtime 的 metrics 不是替代关系:前者更偏应用链路,后者更偏模型服务和 GPU 资源。</p> <p>日志里要谨慎处理 prompt 和输出内容。生产环境通常需要脱敏、采样、权限控制和保留周期策略,否则 observability 很容易变成隐私和合规风险。</p> <h3 id="86-监控技术栈">8.6 监控技术栈</h3> <p>一个常见开源栈可以长这样:</p> <div class="mermaid">flowchart LR V["vLLM / SGLang / Ollama API"] --> P["Prometheus"] D["DCGM Exporter"] --> P N["Node Exporter / cAdvisor"] --> P P --> G["Grafana Dashboard"] P --> A["Alertmanager"] App["App / Gateway"] --> O["OpenTelemetry Collector"] O --> T["Tempo / Jaeger"] O --> L["Loki / Log Backend"] G --> S["SLO / Alert Review"] </div> <p>关键组件:</p> <table> <thead> <tr> <th style="text-align: left">组件</th> <th style="text-align: left">作用</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><a href="https://prometheus.io/docs/introduction/overview/" target="_blank" rel="noopener">Prometheus</a></td> <td style="text-align: left">拉取和存储时间序列 metrics,用 PromQL 查询和报警</td> </tr> <tr> <td style="text-align: left"><a href="https://grafana.com/docs/grafana/latest/introduction/" target="_blank" rel="noopener">Grafana</a></td> <td style="text-align: left">展示 dashboard,关联 metrics、logs、traces</td> </tr> <tr> <td style="text-align: left"><a href="https://opentelemetry.io/docs/what-is-opentelemetry/" target="_blank" rel="noopener">OpenTelemetry</a></td> <td style="text-align: left">标准化 traces、metrics、logs 的采集和导出</td> </tr> <tr> <td style="text-align: left"><a href="https://github.com/NVIDIA/dcgm-exporter" target="_blank" rel="noopener">NVIDIA DCGM Exporter</a></td> <td style="text-align: left">暴露 GPU 利用率、显存、功耗、温度等指标给 Prometheus</td> </tr> <tr> <td style="text-align: left">Loki / Tempo / Jaeger</td> <td style="text-align: left">存储日志和 trace,帮助做请求级问题定位</td> </tr> </tbody> </table> <p>一个最小可用方案是:serving 框架暴露 <code>/metrics</code> 或 Prometheus 格式指标,Prometheus 定时 scrape,Grafana 展示 TTFT/ITL/QPS/GPU/KV cache 面板,Alertmanager 根据 P99、错误率、OOM、显存水位报警。等到业务复杂后,再补 OpenTelemetry tracing 和应用层 LLM observability。</p> <h3 id="87-压测与基准测试">8.7 压测与基准测试</h3> <p>压测是主动制造负载,监控是被动观察线上运行。两者要结合:压测告诉你系统极限和拐点,监控告诉你线上是否正在接近这些拐点。</p> <p>为了让实验可复现、可比较,压测至少固定:</p> <ul> <li>模型名、权重精度、量化方式。</li> <li>serving 框架版本和启动参数。</li> <li>GPU 型号、数量、driver、CUDA。</li> <li>输入 token 长度分布。</li> <li>输出 token 长度分布或 <code>max_tokens</code>。</li> <li>stream / non-stream。</li> <li>并发模式:固定 concurrency、固定 request rate,还是 burst。</li> <li>采样参数:temperature、top_p、stop。</li> <li>warmup 次数和统计窗口。</li> </ul> <p>常见工具:</p> <table> <thead> <tr> <th style="text-align: left">工具</th> <th style="text-align: left">适合做什么</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><a href="https://docs.vllm.ai/en/latest/api/vllm/benchmarks/serve/" target="_blank" rel="noopener">vLLM benchmark</a></td> <td style="text-align: left">vLLM 自带 benchmark,输出 request/input/output throughput、TTFT、TPOT、ITL 等</td> </tr> <tr> <td style="text-align: left"><a href="https://github.com/ai-dynamo/aiperf" target="_blank" rel="noopener">AIPerf</a></td> <td style="text-align: left">新一代 GenAI benchmarking 工具,支持多种 endpoint、数据集、并发和请求率控制</td> </tr> <tr> <td style="text-align: left"><a href="https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/perf_analyzer/genai-perf/README.html" target="_blank" rel="noopener">GenAI-Perf</a></td> <td style="text-align: left">NVIDIA 旧工具,文档已说明逐步转向 AIPerf,但仍是理解 LLM benchmark 指标的好材料</td> </tr> <tr> <td style="text-align: left"><a href="https://grafana.com/docs/k6/latest/" target="_blank" rel="noopener">Grafana k6</a> / Locust</td> <td style="text-align: left">通用 HTTP 负载测试,适合压 API gateway、鉴权、代理层,但需要自己统计 token 指标</td> </tr> </tbody> </table> <p>读 benchmark 结果时,第一反应应该是看 workload,而不是直接比较榜单数字。短问答、长 prompt、长输出、多轮对话、RAG、agent 工具调用,会得到完全不同的 TTFT、ITL、吞吐和显存曲线。</p> <h2 id="9-量化与显存优化">9. 量化与显存优化</h2> <h3 id="91-为什么需要量化">9.1 为什么需要量化</h3> <p>量化的目标是用更少 bit 表示原本高精度的数值。LLM 推理中最常见的是<u>权重量化</u>和 <u>KV cache 量化</u>。</p> <p>最简单的显存估算:</p> <p>$$ M_{\mathrm{weights}} \approx N_{\mathrm{params}} \times \frac{q}{8} $$</p> <p>其中 $q$ 是每个参数的 bit 数。因此,忽略 scale、zero point 等量化元数据时:</p> <p>$$ M_{\text{7B, FP16/BF16}} \approx 7 \times 10^9 \times 2\text{ bytes} \approx 14\text{ GB} $$</p> <p>$$ M_{\text{7B, INT4}} \approx 7 \times 10^9 \times 0.5\text{ bytes} \approx 3.5\text{ GB} $$</p> <p>量化的收益有三类:</p> <ol> <li>省显存:模型能放进更小的 GPU,或同一张 GPU 支持更大 batch。</li> <li>提速度:decode 往往受显存带宽限制,数据更小,搬运更快。</li> <li>降成本:同等负载可以用更少或更便宜的硬件。</li> </ol> <p>代价是精度损失和 kernel 复杂度。不是所有量化都会变快:如果硬件或 runtime 没有高效 kernel,反量化开销可能抵消收益。</p> <h3 id="92-权重量化">9.2 权重量化</h3> <p>权重量化作用于模型固定参数。常见方法:</p> <table> <thead> <tr> <th style="text-align: left">方法</th> <th style="text-align: left">典型格式</th> <th style="text-align: left">关键思想</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><a href="https://arxiv.org/abs/2208.07339" target="_blank" rel="noopener">LLM.int8()</a></td> <td style="text-align: left">INT8 混合精度</td> <td style="text-align: left">大部分用 INT8,离群维度保留高精度</td> </tr> <tr> <td style="text-align: left"><a href="https://arxiv.org/abs/2210.17323" target="_blank" rel="noopener">GPTQ</a></td> <td style="text-align: left">W4/W3</td> <td style="text-align: left">用近似二阶信息补偿量化误差</td> </tr> <tr> <td style="text-align: left"><a href="https://arxiv.org/abs/2306.00978" target="_blank" rel="noopener">AWQ</a></td> <td style="text-align: left">W4A16</td> <td style="text-align: left">根据激活分布保护重要权重通道</td> </tr> <tr> <td style="text-align: left"><a href="https://arxiv.org/abs/2211.10438" target="_blank" rel="noopener">SmoothQuant</a></td> <td style="text-align: left">W8A8</td> <td style="text-align: left">把激活离群值难度迁移到权重上</td> </tr> <tr> <td style="text-align: left"><a href="https://arxiv.org/abs/2305.14314" target="_blank" rel="noopener">QLoRA</a></td> <td style="text-align: left">NF4 + LoRA</td> <td style="text-align: left">冻结 4-bit 基座,只训练低秩适配器</td> </tr> <tr> <td style="text-align: left">GGUF K-quants</td> <td style="text-align: left">Q4_K_M、Q5_K_M 等</td> <td style="text-align: left">llama.cpp/Ollama 本地推理常用格式</td> </tr> </tbody> </table> <p>理解量化可以从一个标尺类比开始:原来用毫米级尺子测长度,现在只给 16 个刻度。刻度越少,记录越省空间,但误差越大。优秀的量化方法就是想办法把刻度放在“最常出现、最重要”的数值区域,而不是平均撒开。</p> <p>在服务端 GPU 上,AWQ/GPTQ/FP8 等是否值得用,要看框架支持和 kernel 效率。vLLM 的 <a href="https://docs.vllm.ai/en/latest/features/quantization/index.html" target="_blank" rel="noopener">quantization 文档</a> 列出了支持的量化后端;Ollama/llama.cpp 则更多围绕 GGUF 量化模型。</p> <h3 id="93-kv-cache-优化">9.3 KV cache 优化</h3> <p>KV cache 不是模型文件里下载来的东西,而是推理时动态产生的。每个请求的每个历史 token,都会在每层 attention 中留下 Key 和 Value,用于后续 token 查询。</p> <p>它解决的是“不要重复读题”的问题:生成第 100 个 token 时,模型不需要重新计算前 99 个 token 的全部中间状态,只需要复用 KV cache。但代价是显存随上下文长度和并发线性增长。</p> <p>这里要和 1.4 节的上下文压缩区分开:上下文压缩是在模型计算前减少输入 token 或历史信息,KV cache 优化是在模型已经保留这些 token 后,降低缓存它们的显存和调度成本。前者改变 prompt,后者改变 runtime 管理方式。</p> <p>KV cache 优化有几类:</p> <ul> <li>dtype 降低:例如 FP16/BF16 改成 FP8 或 INT8。</li> <li>KV cache quantization:如 <a href="https://arxiv.org/abs/2402.02750" target="_blank" rel="noopener">KIVI</a>、<a href="https://arxiv.org/abs/2401.18079" target="_blank" rel="noopener">KVQuant</a> 等研究。</li> <li>prefix cache:复用相同前缀的 KV。</li> <li>paged/block 管理:减少显存碎片,例如 PagedAttention。</li> <li>eviction/offloading:长上下文场景下把部分 KV 移出 GPU 或丢弃不重要 token。</li> </ul> <p>KIVI 的一个重要观察是:Key 和 Value 的分布不同,Key 更适合 per-channel 量化,Value 更适合 per-token 量化。这说明 KV cache 量化不是简单把所有数压成 INT4,而要根据张量统计特性设计。</p> <p>vLLM 已有 <a href="https://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache/" target="_blank" rel="noopener">quantized KV cache</a> 相关支持。实践中,短上下文、低并发场景开启 KV 量化未必更快;长上下文、高并发、显存紧张时,KV cache 优化才更容易体现价值。</p> <h3 id="94-量化选择要问什么">9.4 量化选择要问什么</h3> <p>量化选型可以按下面的问题走:</p> <ol> <li>目标是省权重显存,还是省 KV cache 显存?</li> <li>场景是本地 CPU/消费级 GPU,还是服务端 A100/H100?</li> <li>框架是否原生支持该量化格式?</li> <li>是否有高效 kernel,还是只是“能跑”?</li> <li>精度损失是否在业务可接受范围内?</li> <li>benchmark 是否覆盖真实输入输出长度和并发?</li> </ol> <p>几个经验判断:</p> <ul> <li>本地运行优先看 GGUF 和 Ollama/llama.cpp 生态。</li> <li>单卡显存放不下模型时,先考虑权重量化。</li> <li>长上下文并发撑爆显存时,再重点考虑 KV cache 量化或 prefix cache。</li> <li>H100/Blackwell 等新硬件上,FP8 往往更值得关注。</li> <li>量化后必须重新评估质量,特别是代码、数学、工具调用和结构化输出场景。</li> </ul> <p>最终,量化不是单独的“压缩技巧”,而是 serving 系统设计的一部分。它改变权重大小、KV cache 容量、batch 上限、decode 带宽压力,最终反映到 TTFT、ITL、吞吐和 goodput 上。</p>

2026/7/8
阅读更多

Gemini Cli实现“选中文字添加日程”的右键功能

<h2 id="前言">前言</h2> <p>近来我在摸索将<a href="https://geminicli.com/" target="_blank" rel="noopener">Gemini Cli</a>等终端agents融入工作流,目前总体感觉是——这类工具确实能够将脑力和精力从“杂活”和“需要但不感兴趣”的事情中解放出来,效率提升很多。这篇短文将介绍一个最近的实践案例。</p> <p>作为博士生,我经常需要做的一件事是手动将电子邮件中的会议、报告、答辩等信息添加到Apple日历中。手动设置事件的标题、时间、地点、提前提醒时间、线上开会链接等内容属实麻烦。在ChatGPT出圈后,我一直想要开发一个功能——选中文字后直接在右键菜单中点击“添加日程”,程序就能够调用LLM去解析文字,生成Apple日历事件并导入到日历中。</p> <p>然而,这个想法在脑海中,一直也没有去实现。最近,需要添加的事件尤其多,今晚吃饭回来后又想到了这一茬儿,于是打开Gemini Cli,给出需求,让它设计方案并实现,十分顺利。</p> <p>现在我只需要在邮件中选中文字,点击右键,然后选择“添加日程”:</p> <p><img src="Xnip2026-01-26_19-56-57.jpg" alt="Xnip2026-01-26_19-56-57"></p> <p>稍等片刻,就能在日历中看到对应的事项了:</p> <p><img src="Xnip2026-01-26_19-58-17.jpg" alt="Xnip2026-01-26_19-58-17"></p> <p>代码仓库:<a href="https://github.com/brant-ruan/text2agenda" target="_blank" rel="noopener">https://github.com/brant-ruan/text2agenda</a></p> <h2 id="实践">实践</h2> <p>为了提高成功率,我在Gemini Cli中将模型设定为<code>gemini-3-pro-preview</code>。初始需求描述如下:</p> <pre tabindex="0"><code>我想要在当前目录下开发一个软件或者借助Mac上已有的automator等,它的功能是给我的macbook添加一个右键菜单“添加日程”, 使用场景是选中一段文字后,比如在Outlook中选中一个关于会议日程信息通知邮件的文字部分,右键,选择“添加日程”, 然后自动调用OpenAI/DeepSeek/Gemini等大模型的API,将其解析成.ics(苹果日程格式),然后自动导入到我的系统日历中。 这个项目允许用户通过配置文件配置API KEY等配置项。 </code></pre><p>很快,Gemini给出了实现方案:</p> <pre tabindex="0"><code>I will design a solution using a Python script as the core logic engine (handling the API call and Calendar interaction) and use macOS Automator to trigger it via the right-click menu. Here is the development plan: 1. Project Setup &amp; Configuration * Structure: Create a directory for the project. * Dependencies: Use Python&#39;s requests library to communicate with LLM APIs. * Config: Create a config.json file where you can define your API Key, Base URL (allowing use of OpenAI, DeepSeek, or Gemini via compatible endpoints), and Model name. 2. Core Logic Implementation (remind.py) * Input: The script will accept text arguments (the selected text from your email/document). * Context: It will automatically detect the current system date/time so that relative dates like &#34;next Friday&#34; are parsed correctly. * LLM Processing: It will send the text to the configured API with a prompt to extract event details (Title, Start Time, End Time, Location, Notes) in a strict JSON format. * Calendar Automation: Instead of just generating an .ics file (which requires manual clicking to import), I will use AppleScript (executed via Python) to directly insert the event into your macOS Calendar app. 3. Verification * I will create a dummy test to ensure the script runs and attempts to reach the API (handling errors gracefully if the API key is not yet set). 4. Integration (The &#34;Right-Click&#34; Menu) * Since I cannot interact with your macOS GUI to create the Automator workflow, I will generate the Python script and then provide you with exact, simple steps to create the Automator Quick Action that runs this script. I will start by setting up the project structure and the configuration file. </code></pre><p>方案不错,批准了,请执行!很快,Gemini大哥就完成实现,并且给出了配置指南:</p> <pre tabindex="0"><code>You now have a fully functional backend to parse text into Calendar events using an LLM. Project Files Created * remind.py: The main logic script. * config.json: Configuration file for your API Key. * run_remind.sh: A helper script for Automator to run the Python code easily. * venv/: A Python virtual environment with necessary libraries installed. Step 1: Configure API Key Open the configuration file and paste your OpenAI (or compatible) API Key. open config.json Change `YOUR_API_KEY_HERE` to your actual key. Step 2: Create the &#34;Right-Click&#34; Menu (Automator) Since this part interacts with the macOS UI, you need to perform these steps manually once: 1. Open Automator (cmd+space, type &#34;Automator&#34;). 2. Choose New Document -&gt; Quick Action. 3. At the top, configure: * Workflow receives current Text in Any Application. 4. In the left sidebar, search for Run Shell Script and drag it to the right. 5. Configure the script action: * Shell: /bin/bash * Pass input: as arguments (Important!) * Paste the following command (using the absolute path to your project): /ABSOLUTE_PATH/run_remind.sh &#34;$1&#34; 6. Press Cmd+S to save. Name it Add Event (or &#34;添加日程&#34;). How to Use 1. Select any text containing a date/time (e.g., in an email or website). 2. Right-click the text. 3. Go to Services (or just look at the bottom of the menu) -&gt; Add Event. 4. The script will run in the background, send the text to the LLM, and automatically add the event to your macOS Calendar. Troubleshooting: If nothing happens, check the log file created by the script: cat /tmp/remind_log.txt </code></pre><p>按照它的提示配置完(文字解析使用的是物美价廉的<code>gpt-5-nano</code>),我测试了一下,检查日志,发现了问题,再次交互:</p> <pre tabindex="0"><code>遇到了问题:Analyzing text... Error calling API: 400 Client Error: Bad Request for url: https://api.openai.com/v1/chat/completions Failed to extract event data. 我建议你使用litellm来调用,而非自己手写request </code></pre><p>Gemini很快完成了基于<a href="https://github.com/BerriAI/litellm" target="_blank" rel="noopener">LiteLLM</a>的实现。我再次提出需求:</p> <pre tabindex="0"><code>现在就不必在配置文件中添加base_url了,不过我希望配置文件中支持设置提前几分钟提醒。 </code></pre><p>Gemini再次完成实现。现在,整个流程已经很流畅了。不过,我还希望更方便一些:</p> <pre tabindex="0"><code> 我怎么才能将这个右键菜单从“Services”中移出来到一级菜单? </code></pre><p>Gemini大哥不厌其烦地给出指点:</p> <pre tabindex="0"><code>在 macOS 系统中,原生不支持用户直接将 Automator (自动操作) 创建的“快速操作”强行固定在右键的一级菜单中。macOS 会根据你启用的服务数量,自动决定是将它们折叠进“服务 (Services)”子菜单,还是显示在菜单底部。 要想实现“在一级菜单直接点击”,通常有以下两种方案: 方案一:清理“服务”菜单(原生,但不保证 100% 成功) macOS 的逻辑是:如果启用的“服务”很少,它有时会将它们直接显示在右键菜单底部,而不折叠进子菜单。你可以尝试禁用不用的服务: 1. 打开 系统设置 (System Settings) -&gt; 键盘 (Keyboard)。 2. 点击 键盘快捷键 (Keyboard Shortcuts) -&gt; 服务 (Services)。 3. 在“文本 (Text)”分类下,取消勾选你不常用的所有服务,只保留刚才创建的“Add Event”。 4. 重启应用(如浏览器或邮件)看是否生效。 ... </code></pre><p>我再次尝试,发现了一个问题——设置的提前15分钟变成了15小时,于是再次发问:</p> <pre tabindex="0"><code>现在似乎填写的提前15分钟最终在日历中变成了提前15小时,请改正。另外,我希望生成的事件能够体现出事件的本质特征。 例如,对于下面的文字,我希望生成的事件标题为Anonymous&#39;s PH.D Defense之类,而不是他的defense题目(...): PH.D DEFENCE - PUBLIC SEMINAR Title: ... Speaker: Mr. Anonymous Date/Time: 1 February 2026, Monday, 03:00 PM to 04:30 PM Venue: XXXX, XXXX XX-XX Advisor: Dr XXX, XXX, School of Computing </code></pre><p>Gemini再次更新代码中的prompt。至此,这个小玩意儿已经完全满足了我的使用需求。</p>

2026/1/26
阅读更多

VPSS:面向软件供应链的漏洞影响评估

<h2 id="简介">简介</h2> <p>本文在“安全研究GoSSIP”公众号上<a href="https://mp.weixin.qq.com/s/DKuBtVP7QAK6criLrcC0Mg" target="_blank" rel="noopener">首发</a>。</p> <p>老读者从首尔发回报道——11月17日到19日,第四十届软件工程领域国际会议ASE 2025在韩国首尔顺利举行。今年的ASE会议一共收到了1181篇投稿,最终录用了246篇论文。其中,来自中国的作者占比超过60%(含中国大陆及港澳台),体现了在软件工程领域的强劲实力。在所有提交和接收的论文中,研究主题的Top 3均为“AI和SE”、“测试和分析”、“安全及其他非功能特性”。</p> <p>今天为大家推荐的论文是发表在这次ASE上的,一篇关于软件供应链漏洞影响评估的工作:<strong>Propagation-Based Vulnerability Impact Assessment for Software Supply Chains</strong>。该工作由新加坡国立大学梁振凯研究组完成并投稿。</p> <p>随着软件生态变得日益复杂,软件供应链中的漏洞不仅影响漏洞所在的项目,还为依赖其所在项目的下游项目带来了安全风险——即使下游项目业务自身安全,攻击者还是能够通过利用这些上游漏洞来达到攻击下游项目的目的。因此,识别一个漏洞在软件供应链中的影响范围和规模变得越来越重要。</p> <p>然而,现有工作并不能满足这一需求。一方面,已有研究要么不精确——只考虑了软件包层级(package-level)的依赖关系,没有进一步考虑漏洞所在函数是否真正可达,从而引入了大量虚假告警,要么是不完整的——通过在软件生态中采样等方式考察了局部或部分的函数调用图级(call-graph-level)依赖关系;另一方面,当前已被广泛采用的漏洞评估指标,如CVSS、CWE等,都只能用于对漏洞本身进行评估,而无法反映漏洞在软件供应链中的影响。</p> <p>为了实现精确、完整的软件供应链漏洞影响评估,受程序分析中的数据流分析算法启发,论文首先提出了一个基于worklist并支持多层级剪枝的漏洞传播分析算法,能够精确、高效地在函数调用图层级、面向漏洞所在整个软件供应链生态实现漏洞传播分析。在此基础上,论文进一步提出了漏洞传播评分系统(Vulnerability Propagation Scoring System,简称VPSS),对漏洞传播分析结果进行建模,帮助人们对漏洞在软件生态中的影响进行量化和感知,支撑更加科学、及时的漏洞治理工作。</p> <p>论文作者在Java Maven生态系统上实现了工具原型,并使用100个Maven生态中的漏洞进行了测试。评估结果显示,论文提出的算法能够完成全Maven生态的漏洞影响评估,相应的VPSS分数能够提供有价值的漏洞影响规模情报。</p> <p>该论文的贡献主要包括:</p> <ul> <li>设计了一种新的漏洞传播分析算法,实现在函数调用图层级、面向漏洞所在整个软件供应链生态识别受漏洞影响的下游依赖项目。</li> <li>提出了漏洞传播评分系统(Vulnerability Propagation Scoring System,简称VPSS),这是第一个专门用于量化软件供应链中漏洞影响的动态评估指标。</li> <li>在Java Maven生态系统上实现了算法和指标的工具原型,并使用100个Java Maven生态中的漏洞进行了测试。工具原型和用于测试的漏洞数据集已经<a href="https://github.com/brant-ruan/vpss" target="_blank" rel="noopener">开源</a>。</li> </ul> <h2 id="背景">背景</h2> <p>软件供应链安全已成为信息安全领域的焦点。一个上游库的漏洞(例如Heartbleed 或 Log4Shell )就可能使其下游成千上万的软件项目处于风险之中 。因此,在漏洞被披露后,快速准确地评估其“影响范围”(Impact Scope)至关重要 。然而,现有的研究在尝试解决这个问题时,面临两大局限:</p> <p><strong>缺乏准确且完整的传播分析:</strong></p> <ol> <li>粒度太粗:大多数分析停留在“包级别”(package-level)。它们只检查下游项目是否“声明”或包含了对脆弱包的依赖。但这会带来大量的假阳性(false positives),因为下游项目很可能只是依赖了那个包,却根本没有调用到漏洞所在的脆弱函数(vulnerable function,VF)。</li> <li>范围太窄:另一部分研究虽然尝试了更精确的“调用图”(call graph, CG)级别分析,但它们的能力非常受限——要么无法处理复杂的“全生态系统”依赖 ,要么只分析有限的下游项目 ,并且常常忽略了“传递性依赖”带来的深层风险。</li> </ol> <p>作者进一步从六个方面衡量、对比了现有工作,并得出理想的漏洞传播分析应该具备的特性:</p> <p><img src="related.jpg" alt="related"></p> <p><strong>缺乏量化供应链“传播影响”的指标</strong>:例如,我们熟知的CVSS评分系统,是用来评估漏洞本身的固有特征(如攻击复杂度、所需权限等),但它无法衡量漏洞在供应链中的“传播影响”。一个CVSS 9.8分的漏洞,如果只存在于一个无人问津的库中,其真实的供应链风险可能远小于一个7.5分但存在于核心基础库中的漏洞。CVSS v4.0的官方FAQ也明确承认了CVSS不适用于衡量供应链的传播影响。</p> <p>针对上述痛点,作者希望设计一个既准确(CG级别)又完整(全生态、传递性)的分析框架,并提出一个专门用于量化传播影响的新指标。</p> <h2 id="研究方法">研究方法</h2> <p>为了实现这一目标,作者设计了一个包含四个步骤的语言无关的自动化漏洞影响评估框架,如下图所示:</p> <p><img src="overview.jpg" alt="overview"></p> <h3 id="一依赖图构建-dependency-graph-construction">一、依赖图构建 (Dependency Graph Construction)</h3> <p>首先,作者需要构建一个描述目标软件生态中全部依赖关系的依赖图。但如果直接构建“项目-版本”(PV)级别的图,规模将大到无法处理(例如Java Maven生态有超过1500万个PV )。作者的关键洞察是:转而构建P-level(项目级别)的依赖图。这个图的规模要小得多(Maven中约66万个P ),可以作为高效的预过滤器 ,同时将具体的版本依赖信息(deps.json)用于后续的精确查询 。这个依赖图只需要构建一次,然后随着软件生态官方仓库做动态增量更新即可。</p> <h3 id="二脆弱函数识别-vulnerable-function-identification">二、脆弱函数识别 (Vulnerable Function Identification)</h3> <p>为了进行CG级别的分析,我们必须首先知道“漏洞在哪几个函数里”。对此,作者采用了一种LLM辅助的优化策略 :首先,使用传统的patch-based方法从补丁中提取候选的VF。然后,利用LLM(作者测试了GPT-4o-mini等模型)强大的代码语义理解能力,自动过滤掉那些与漏洞无关的修改,包括“语义等价的修改”(如重命名变量、增删空格或注释等)和“语义改变但无关的修改”(如增删日志(logging)代码等)。</p> <p><img src="vfi.jpg" alt="vfi"></p> <h3 id="三漏洞传播分析-vulnerability-propagation-analysis">三、漏洞传播分析 (Vulnerability Propagation Analysis)</h3> <p>这是作者提出的分析框架的核心。他们设计了一种新颖的“分层基于工作列表(Hierarchical Worklist-based)”的算法:</p> <p><img src="propagation.jpg" alt="propagation"></p> <p>为什么用Worklist?论文的算法借鉴了数据流分析中的经典“工作列表”思想 。这使作者能系统地遍历整个生态依赖图,有效处理复杂的共享依赖,甚至是真实世界中存在的“循环依赖”问题 ,确保分析的健全性(soundness)。</p> <p>为什么是Hierarchical(分层)?这是为了效率。直接对全生态构建CG进行分析是计算上不可行的 。作者设计了三级“分层剪枝(Hierarchical Pruning)”策略 ,逐层过滤掉海量的“假阳性”依赖关系:</p> <ul> <li>版本剪枝(Version-based Pruning):检查下游项目是否依赖了受影响的上游版本 。</li> <li>导入剪枝(Import-based Pruning):检查下游项目是否 真正导入(import)了上游包的内容 。在这一步,作者还通过识别“内在范围”(intrinsic scope),巧妙地处理了“fat JAR”等打包问题。</li> <li>CG级别剪枝(CG-level Pruning): 最后,作者才对剩下的候选PV构建CG,检查下游项目是否真正调用了上游的脆弱入口点(entry-points)。</li> </ul> <h3 id="四vpss计算-vpss-calculation">四、VPSS计算 (VPSS Calculation)</h3> <p>在精确分析完传播范围后,作者引入了一个全新的量化指标:VPSS(Vulnerability Propagation Scoring System),用于量化漏洞在供应链中的影响范围和演变。它和CVSS一样,采用0-10分制,并分为低、中、高、危急四个等级,易于理解和使用。注意,VPSS是“时间感知(Time-aware)”的,因此可以计算漏洞在任意时间点(例如t)的VPSS分数(计算时会排除t之后发布的P和PV),从而动态追踪其影响力的演变。</p> <p>VPSS的核心公式是VPSS_raw = PBF x PDF,其中:</p> <ul> <li>PBF(传播广度因子): 衡量漏洞传播的宽度。它综合考虑了受影响的下游P和PV的数量(包括直接和传递依赖)。</li> <li>PDF(传播深度因子): 衡量漏洞传播的深度。它基于传播链的最大和平均长度来计算 。</li> </ul> <p>详细的计算过程如下:</p> <p><img src="vpss.jpg" alt="vpss"></p> <h2 id="评估">评估</h2> <p>作者在Java Maven软件生态上实现了分析框架的原型 ,并抽取了100个真实CVE进行评估 。</p> <h3 id="一漏洞传播分析的有效性和可扩展性如何">一、漏洞传播分析的有效性和可扩展性如何?</h3> <p>发现1:分层剪枝策略效果显著。在分析100个CVE的过程中,平均97.8%的项目(P)和99.2%的项目版本(PV)被成功剪枝。</p> <p>发现2:假阳性比例大。如下图所示,如果仅基于依赖声明(即“initial”状态),平均一个漏洞会涉及上千个项目和数万个PV。而作者的分析(v3)最终确认,高达94.9%的PV是假阳性(即它们虽然依赖了,但根本没调用)。这大大降低了CG的构建成本 。</p> <p><img src="eval1.jpg" alt="eval1"></p> <h3 id="二vpss分数能告诉我们什么">二、VPSS分数能告诉我们什么?</h3> <p>作者计算了这100个CVE在披露后24个月内(每30天一个快照,t0到t23)的VPSS分数演变。</p> <p>发现1:VPSS能动态反映生态修复趋势。如下图所示,绝大多数漏洞的VPSS分数都随时间推移呈下降趋势。这完全符合预期:随着补丁被应用、下游项目逐渐迁移到安全版本,漏洞的实际传播影响在不断减小。</p> <p>发现2:VPSS能捕获异常的“延迟更新”现象。作者也发现了一个有趣的特例:CVE-2016-3086的分数在t1时刻反而异常上升了(从6.95升至7.19)。作者推测这可能是由于下游项目“延迟更新”其依赖所导致的。</p> <p><img src="eval2.jpg" alt="eval2"></p> <p>发现3:大部分漏洞的传播影响有限。如下面的箱线图所示,在100个CVE中,绝大多数的VPSS分数都保持在较低水平,并在两年内趋于稳定。</p> <p><img src="eval3.jpg" alt="eval3"></p> <h3 id="三案例分析cve-2016-5393">三、案例分析:CVE-2016-5393</h3> <p>CVE-2016-5393在测试数据集中VPSS得分最高(t0时为7.35)。作者分析发现,它在披露时直接影响了228个项目,并传递影响了另外154个项目。其最长传播链达到了7跳,并且在24个月后分数依然高达6.86,反映了生态中“长尾依赖”的顽固性。</p> <p><img src="eval4.jpg" alt="eval4"></p> <p>论文链接:<a href="https://arxiv.org/pdf/2506.01342" target="_blank" rel="noopener">https://arxiv.org/pdf/2506.01342</a></p> <p>VPSS开源项目链接:<a href="https://github.com/brant-ruan/vpss" target="_blank" rel="noopener">https://github.com/brant-ruan/vpss</a></p> <p>作者团队十分欢迎大家交流合作,一起推动软件供应链漏洞治理工作!</p> <h2 id="作者信息">作者信息</h2> <p>投稿作者简介:阮博男,2024级新加坡国立大学博士生,导师为梁振凯老师,目前主要研究方向为漏洞挖掘和漏洞评估,相关研究成果发表在ASE、USENIX Security、RAID等软件工程和信息安全领域的国际会议上,曾获得RAID 2024最佳实践论文奖和USENIX Security 2025杰出论文奖,也曾在BlackHat Asia、KCon、CIS等国内外知名工业界信息安全会议上发表演讲。</p> <p>个人主页:<a href="https://profile.wohin.me/" target="_blank" rel="noopener">https://profile.wohin.me</a></p> <p>如果对NUS Curiosity Research Group感兴趣,梁振凯老师也在招收系统安全方向的博士生和博士后,详细信息可以参考:</p> <p>梁振凯老师的个人主页:<a href="https://www.comp.nus.edu.sg/~liangzk" target="_blank" rel="noopener">https://www.comp.nus.edu.sg/~liangzk</a></p> <p>NUS Curiosity研究小组主页:<a href="https://nus-curiosity.github.io" target="_blank" rel="noopener">https://nus-curiosity.github.io</a></p>

2025/12/2
阅读更多

很多年

<p>作者:北岛</p> <hr> <p>这是你,这是<br> 被飞翔的阴影困扰的 你忽明忽暗<br> 我不再走向你 寒冷也让我失望<br> 很多年 冰山形成以前<br> 鱼曾浮出水面 沉下去,很多年<br> 我小心翼翼<br> 穿过缓缓流动的夜晚 灯火在钢叉上闪烁<br> 很多年 寂寞 这没有钟的房间<br> 离去的人也会带上 钥匙,很多年<br> 在浓雾中吹起口哨<br> 桥上的火车驰过 一个个季节<br> 从田野的小车站出发 为每棵树逗留<br> 花开结果,很多年</p>

2025/9/13
阅读更多

我的母亲

<p>作者:安老三</p> <hr> <p>今天,我巧遇拍短视频采访,还需要抓题,因为我是这群工友中唯一上过高中的,两个女孩子就选中了我,我有幸抓到了“我的母亲”这个标题,重温母亲的回忆,让我思绪万千。</p> <p>母亲走了三十多年了,就埋在村头的老坟地里,她的模样、她的一举一动就跟在眼前似的。</p> <p>母亲啊,一辈子就没闲过,天不亮就起,摸着黑才歇。家里家外操不完的心,穿的永远是洗得发白、补了又补的衣裳。她心善能容人,跟邻里没红过脸,有啥好东西总先紧着别人。</p> <p>最让我忘不了的是吃饭,一大家子人围着桌子,母亲从来不上桌,她就在灶台边忙活,等我们都吃完了,她才瞅瞅锅里,剩下了,就扒拉两口,要是没剩,她就不吃了,说不饿。那会儿日子都紧巴,穷富差不了多少,可人心热乎,讲情义,母亲就是那最厚道的一个。她包的饺子,那叫一个香,是穷日子里最盼的念想。</p> <p>过年是她最累的时候,洗洗涮涮,准备年饭,累得腰都直不起来,可脸上总挂着笑,看着我们放炮仗那股高兴劲儿,她比我们还乐呵。再破的衣裳,经她的手也给我们拾掇得干干净净。</p> <p>那时候还是集体,一年四季都出不到一点新鲜菜,到秋天就要分口粮,白天打下谷子,晚上分给每家每户,这一分就是一个大晚上。农村的秋天还是一个很冷的秋天,母亲身上穿得很薄,粮食分到家以后,母亲这一晚上冻得受不了,她那薄薄的被子围在身上一会儿,天就亮了。母亲一天的辛劳又开始了,她揉揉眼,又得爬起来给一大家子弄吃的。家里人多,那口烧柴火的大铁锅死沉死沉,端上端下,真不是一般人能干的力气活。现在想想,母亲那瘦小的身子,哪来那么大劲儿。</p> <p>母亲走的时候才五十出头,是累病的。如今她的坟就在村边,一个不起眼的小土包。每次回村,我总要去那儿站站,跟她说说话。我在城里干了大半辈子苦力,扛水泥、扎钢筋、爬脚手架,手上肩上全是老茧,累是真累。可想想母亲当年端那口大铁锅的劲儿,想想她冻得发抖还咬牙撑到天亮的样子,我这力气就又上来了。母亲没享过福,可她教会我的就是这骨子里的硬气和对家的担当。我得把您撑起来的这个家接着撑下去,撑稳当。</p> <p>坟头上的草青了又黄,黄了又青,就像我的念想一样,一年年总也断不了。我已经当了爸爸,也已经当了爷爷,但我已经三十多年没叫过妈妈了。我想着,等哪天我扛不动水泥了,就回村里挨着那堆土躺下,没准那时候我再叫妈妈,她就能听见了。</p>

2025/7/21
阅读更多

致谢

<p>作者:黄国平</p> <hr> <p>我走了很远的路,吃了很多的苦,才将这份博士学位论文送到你的面前。二十二载求学路,一路风雨泥泞,许多不容易。如梦一场,仿佛昨天家人才团聚过。</p> <p>出生在一个小山坳里,母亲在我十二岁时离家。父亲在家的日子不多,即便在我病得不能自己去医院的时候,也仅是留下勉强够治病的钱后又走了。我十七岁时,他因交通事故离世后,我哭得稀里糊涂,因为再得重病时没有谁来管我了。同年,和我住在一起的婆婆病故,真的无能为力。她照顾我十七年,下葬时却仅是一副薄薄的棺材。另一个家庭成员是老狗小花,为父亲和婆婆守过坟,后因我进城上高中而命不知何时何处所终。如兄长般的计算机启蒙老师◼️◼️没能看到我的大学录取通知书,对我照顾有加的师母也在不惑之前匆匆离开人世。每次回去看他们,这一座座坟茔都提示着生命的每一分钟都弥足珍贵。</p> <p>人情冷暖,生离死别,固然让人痛苦与无奈,而贫穷则可能让人失去希望。家徒四壁,在煤油灯下写作业或者读书都是晚上最开心的力。如果下雨,保留节目就是用竹笋壳塞瓦缝防漏雨。高中之前的主要经济来源是夜里抓黄鳝、周末钓鱼、养小猪崽和出租水牛。那些年里,方圆十公里的水田和小河都被我用脚测量过无数次。被狗和蛇追,半夜落水,因蓄电瓶进水而摸黑逃回家中:学费没交,黄鳍却被父亲偷卖了,然后买了肉和酒,都是难以避免的事。</p> <p>人后的苦尚旦还能克服,人前的尊严却无比脆弱。上课的时候,因拖欠学费而经常被老师叫出教室约谈。雨天湿漉着上课,屁股后面说不定还是泥。夏天光着脚走在滚烫的路上。冬天穿着破旧衣服打着寒颤穿过那条长长的过道领作业本。这些都可能成为压垮骆驼的最后一根稻草。如果不是考试后常能从主席台领奖金,顺便能贴一墙奖状满足最后的虚荣心,我可能早已放弃。</p> <p>身处命运的旋涡,耗尽心力去争取那些可能本就是稀松平常的东西,每次转折都显得那么的身不由己。幸运的是,命运到底还有一丝怜惜。进入高中后,学校免了全部学杂费,■■■一家帮助解决了生活费。进入大学后,计算机终于成了我一生的事业与希望,胃溃疡和胃出血也终与我作别。</p> <p>我很庆幸保研时选择了自动化所,感谢研究生部的老师们将我从别的部门调剂到模式识别实验室,感谢导师宗成庆老师选择了我,宗老师将我引入了科学研究的大门,博士这五年无疑是我过去最幸福的时光。惭愧的是,离宗老师的期望显然还有很远的距离,我也知道本可以做得更好。这一段经历已经成为我这一生值得回味的美好瞬间之一。我很喜欢人机交互式机器翻译这个题目,但也仅开了个头。在未来,希望能有机会弥补这段遗憾。</p> <p>从家出发坐大巴需要两个半小时才能到县城,一直盼着走出大山。从炬光乡小学、大寅镇中学、仪陇县中学、绵阳市南山中学,到重庆的西南大学,再到中科院自动化所,我也记不清有多少次因为现实的压力而觉得自己快扛不下去了。这一路,信念很简单。把书念下去,然后走出去,不枉活一世。世事难科,未来注定还会面对更为复杂的局面。但因为有了这些点点滴滴,我已经有勇气和耐心面对任何困难和挑战。理想不伟大,只愿年过半百,归来仍是少年,希望还有机会重新认识这个世界,不辜负这一生吃过的苦。最后如果还能做出点让别人生活更美好的事,那这辈子就赚了。</p> <p>最近这些年,特别感谢■■■■■■■■■■■■■■■■重塑了我的人生。</p> <p>感谢■■■老师和■■■老师为我的研究提出诸多建设性的意见和建议。与他们的交流过程中,我也被他们坦诚为人、踏实做事的处事风格所感染。感谢所有和我一起在课题组工作的老师们和学习的同学们:■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■。我们在研究上共同努力,互相学习,如亲兄弟姐妹一般谈笑风生。不认大学将来在休息,我想我们都不会忘记这些年一起度过的时光。感谢课题组■■■女士,感谢模式识别综合办实验室的■■■、■■■和■■■等老师,感谢研究生部的■■■、■■■、■■■和■■■等老师,他们为我的学习和工作给予了极大的帮助。这会成为我日后工作的动力,希望最后能成为课题组值得骄傲的学生。</p> <p>感谢父母给了我生命,感谢婆婆和叔辈对我的抚养,感谢■■■■■■■■■■■■■■■■■■对我的牵挂,感谢■■■和■■■对我的支持,感谢一路上让我生活变得美好或者不那么美好的人和事。</p> <p>最后,对参加论文评审、答辩的各位老师表示衷心的感谢!</p>

2025/7/21
阅读更多

怎样当好一名师长

<p>作者:林彪</p> <p>杨成武:一九三六年十二月,我在抗大学习以后回到前方工作的时候,林军团长告诉我:中央、军委决定我由师政治委员改做师长工作,当时我问军团长说,我当不好师长。在我的要求下,林军团长专门同我讲了一次怎样当师长的问题,讲的很深刻很精辟,几十年来,林军团长的这个指示始终鲜明地烙印在我的脑海里。我一直运用它来鞭策自己,检查自己,作为我学习奋斗的准则。林军团长一共讲了九条。这里是根据当时谈话笔记整理的。</p> <hr> <p><b>一、要勤快。</b>不勤快的人办不好事情,不能当好军事指挥员。应该自己干的事情一定要亲自过问,亲自动手。比如,应该上去看的山头就要爬上去,应该自己了解的事情就要及时了解,应该检查的问题就要严格检查。不能懒,军事指挥员切记懒,因为懒会带来危险,带来失败。比方说,一个军事指挥员,到了宿营地就进房子,搞水洗脚洗脸,搞鸡蛋煮面吃,吃饱了就睡大觉。他对住的村子有多大,在什么位置,附近有几个山头,周围有几条路,敌情怎么样,群众条件怎么样,可能发生什么情况,部队到齐了没有,哨位在什么地方,发生紧急情况的处理方案如何,都不过问,都不知道。这样,如果三更半夜发生了情况,敌人来个突然袭击,就没有办法了。到那时候,即使平时很勇敢的指挥员,也会束手无策,只好三十六计,跑为上计,结果变成一个机会主义者。机会主义和打败仗,常常是因为没有思想准备,没有组织准备,工作没做到家,懒的结果。因此,不论大小指挥员都要勤快,要不惜走路,不怕劳累,要多用脑子,要做到心到、眼到、口到、脚到、手到。事情没有做好之前,不能贪闲。贪闲就隐伏着犯错误的根子。什么事情都要心中有底,“凡事预则立,不预则废。”雷打不动的干部,牛皮糖式的干部,不管有多大本事,都不是好干部。我最喜欢勤快的干部,提倡勤快。</p> <p><b>二、要摸清上级的意图。</b>对上级的意图要真正理解,真正融会贯通,真正认识自己所领受的任务在战役、战斗全局中的地位和作用。这样,才能充分发挥自己的主观能动性,才能打破框框,有敢于和善于在新情况中找到新办法的创造性;才能有大勇,才能决心强、决心狠,敢于彻底胜利,有强烈的吃掉敌人的企图和雄心。指挥员的勇敢集中表现在歼敌决心的坚定顽强上面;指挥员的大勇建立在革命的最高自觉性和正确理解上级意图的基础上面。</p> <p><b>三、要调查研究。</b>没有调查研究就没有发言权,这是毛主席早就讲了的。对于敌情、地形、部队的情况和社会的情况要做到心中有数,要天天摸,天天琢磨,不能间断,这样做不能看作是重复,而是不断深化不断提高的过程,是取得正确认识的必不可少的手段。平时积累的掌握的情况越多,越系统,在战时,特别是在紧张复杂的情况下,就越沉着,越有办法,急中生智的“智”才有基础。因此,调查研究工作要贯穿在各项工作之中,要贯穿在每一次战役、战斗的整个过程,反对打莽种仗、反对急性病、反对不亲自动手做调查研究的懒汉作风。特别是敌情必须切实摸透,因为敌情是活的。敌人必然会极力隐蔽,伪装他们的真实企图和决心。要尽一切可能不间断地侦察,查清敌人的部署和动向,看他扮演的是什么角色?是主角还是配角?是主力还是非主力?是骄兵还是败兵?能集中多大兵力向我进攻和阻挡我们的进攻?查明敌军官的特性,看他惯用和擅长用什么战法,根据他当前的企图判断他可能采用什么战法等等。只要摸清了敌情、我情、地势的底,决心就快、就硬、就坚决,就不会被任何困难吓倒。如果情况不清,就会犹豫不定,举棋不定,坐失良机,或者勉强下了决心,一遇风吹草动,听到艰难叫苦或不正确的建议,就容易动摇,可能一分之差,前功尽弃。调查研究也包括多读一些书,首先就要研究和读好毛主席的“中国革命的战争战略问题”和毛主席的各项指示。不仅要读革命的书,还应读敌人的书,研究敌人,经常总结经验教训,这样自己才有锐利的思想武器,才有针对敌情克敌制胜的好办法,把力量用到刀刃上。</p> <p><b>四、要有活地图。</b>指挥员和参谋必须熟悉地图,要经常读地图,熟识地图可以产生见解、产生智慧、产生办法、产生信心。读的办法要把地图挂起来,搬个凳子坐下来对着地图看,看要从大的方向到活动地区,从地区全貌到每一块地形的地形特点,从粗读到细读,逐地逐块地读,用红蓝铅笔把主要的山脉、河流、城镇、村庄和道路标划出来,边划边读,等到地图差不多划烂了也差不多把地图背熟了,背出来了。在熟读地图的基础上,要亲自组织有关指挥员和参谋对作战地区和战场进行实地勘察,校正地图,把战场的地形情况和敌我双方兵力部署都装到脑子里去,做到闭上眼睛面前就有一幅鲜明的战场图景,离开地图也能指挥作战。这样,在你死我活,瞬息万变的战斗情况下可以比敌人来得快,争取先机,先敌一着,掌握主动,稳操胜卷。你们强渡乌江的战斗所以能够很快突过去,就是因为在战前熟悉地图,细致地进行了战场勘测,正确地选择了渡河点。</p> <p><b>五、要把各方面的问题想够想透。</b>每一次战役战斗的组织,要让大家提出各种可能发现的问题,要让大家来找答案,把所有提出的问题都回答了,再没有问题可回答了,这样打起仗来才不会犯大错误,万一犯了错误,也比较容易纠正。没有得到答案的问题,不能因为想了很久想不出来就把它丢开,留一个疙瘩。如果这样是很危险的。在紧要关头这个疙瘩很可能冒出来,就会使你心中无数,措手不及。当然在战争环境中,要考虑的问题很多,不可能一次都提完,也不可能一次都回答完。整个战役战斗的过程,就是不断地提出问题和不断地回答问题的过程。有时脑子里很疲劳,有的问题可能立即回答不了,这时除了好好地和别人商量之外,就好好地睡觉,睡好了睡醒了,头脑就清醒了,再躺在床上好好想一想,就可能开窍,可能想通了,回答了,解决了。总之,对每一个问题都不能含糊了事,问题回答完了,战役、战斗的组织才算完成。</p> <p><b>六、要及时下达决心。</b>在什么样的情况下可以下决心打呢?指挥员必须以最大的努力去组织战役,战斗的准备工作,力求确有把握才动手,不打无把握无准备之仗。但是任何一次战斗都不可能完全具备各种条件,不可能有百分之百的把握。一般说有百分之八十左右的把握就很不错了,就要坚决地打,放手地打。不足的条件要通过充分发挥人的因素作用,依靠人民群众的力量,充分发挥人民军队特有的政治上的优势,充分发挥指挥员的智慧和英勇顽强的战斗作风来弥补,以主观努力来创造条件,化冒险性为创造性取得胜利。敌人的士兵多是穷人出身,同革命军队打仗,它的战斗力不会强,不能同我们比。你看,泸定桥不是冒过去了?直罗镇那个晚上的战斗,不是以一个连打垮了敌人的一个团的进攻?一点险都不敢冒就打胜仗,世界上还没有这样的战例。我这样讲不是提倡了冒险主义,不是无根据的冒险,这样冒险是有根据的。其实这样并非冒险,根据就是我们的部队是党领导的,是毛主席教养出来的部队,不怕苦、不怕死,最勇敢,最聪明,最有纪律,最坚决执行命令,有很高的政治觉悟,我们是革命的军队,打仗有人民群众积极支持,人民群众是站在我们这一边,是帮助我们的,我们依靠人民群众,就可能把敌人封锁起来,就可以把敌人变成聋子、瞎子、傻子。这种政治上的优势就是我们所独有,在战争中可以转化为一种巨大的物质力量,有了这一条,有百分之七八十的把握,就可以下决心打。对于比较弱的敌人,对没有或缺乏准备的敌人,有百分之六十的把握也可以打,可以把它消灭。</p> <p><b>七、要有一个很好的很团结的班子。</b>领导班子的思想认识一致,行动要协调、合拍,要雷厉风行,要有革命英雄主义的气概。都要勤快,都要千方百计地办好事请,完成任务,不相互扯皮,不相互干扰,抱旁观者的态度。如果领导班子不好,人多不但无用,反而有害,所以,班子问题,从领导角度来讲,是一个重要的问题,是做好工作,带好部队打好仗的关键。</p> <p><b>八、要有一个好的战斗作风。</b>有好的战斗作风的部队才能打好仗,打胜仗。好的战斗作风首先是不怕苦,抢着去担负最艰巨的任务,英勇顽强,不怕牺牲,猛打猛冲猛追。特别要勇于穷追。因为把敌人打跨以后,追击是解决战斗,扩大战果,彻底消灭敌人的最关键的一着,在追击的时候,要跑步追,快步追,走不动的扶着拐棍追,就是爬、滚也要往前追,只有抓住敌人,才能消灭敌人。好的战斗作风要平时养成,要靠实际锻炼,要在紧张的残酷的战斗中才能锻炼出来。不敢打硬仗、恶仗的部队让他打几仗就打出来了,因为已经见过恶仗、硬仗的场面,有了体会,有了经验,知道怎么打了,百炼成钢就是这个道理。做工作也要有好的作风,说了就要做,说到哪里就做到哪里,要做到干净利索,要一竿子插到底,一点不含糊,不做好不撒手。好的作风养成关键在于干部。强将手下无弱兵,干部的作风怎么样,部队的作风就会怎样。因此,首先,要抓好干部,要干部做好样子,影响带动部队,或者原来基础较弱的部队,也会很快打出好的作风来,像铁锤子一样,砸到哪里,哪里就碎。</p> <p><b>九、要重视政治,亲自做政治工作。</b>部队战斗力的提高,要靠平时加强党的领导,坚强的政治工作连队的支部一定要建设好。支部工作要作活,就是要把所有的党团员的革命干劲鼓得足足的,充分发挥他们模范带头作用,通过他们把全连带动起来,通过他们去做政治思想工作,提高全体指挥员的阶级觉悟,有了坚强的党支部的领导,有了坚强的政治工作,就可以做到一呼百应,争前恐后,不怕牺牲、前扑后续。战术、技术也要练好,特别是技术,如果枪打不准,战场上就不能消灭敌人,不能解决战斗,因此,军事训练不能马虎。党政工作要领导好训练。艺高人胆大,部队有了高度的无产阶级觉悟,有了好的战斗作风,再加上过硬的作战本领,就如虎添翼,就可以无敌于天下。军事指导员任何时候都不能忘记政治,要亲自做政治工作,调一批政治干部去做军事干部,不仅不会削弱政治工作,实际上是加强了部队的政治工作。</p>

2025/7/14
阅读更多

临江仙·梦后楼台高锁

<p>作者:晏几道</p> <hr> <p>梦后楼台高锁,酒醒帘幕低垂。去年春恨却来时。落花人独立,微雨燕双飞。</p> <p>记得小蘋初见,两重心字罗衣。琵琶弦上说相思。当时明月在,曾照彩云归。</p>

2025/5/31
阅读更多

庐山烟雨浙江潮

<p>作者:苏轼</p> <hr> <p align="center"> 庐山烟雨浙江潮, <br /> 未至千般恨不消。 <br /> 到得还来无别事, <br /> 庐山烟雨浙江潮。 </p>

2025/5/31
阅读更多