MoreRSS

site iconFernweh修改

主要从事系统安全、云安全和AI4Security研究。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Fernweh的 RSS 预览

文章分类与统计

2026-07-11 08:00:00

已发布文章总数:324
最近一次发布日期:2026-07-08

Category Last Updated Date Count
随笔与诗 2025-02-09 103
漏洞研究 2025-12-02 56
攻防实战 2022-10-20 44
混杂分类 2026-01-26 33
云计算与安全 2022-09-19 28
程序分析 2024-04-20 21
读书笔记 2023-11-04 12
操作系统 2023-03-22 10
编程语言 2019-08-10 8
人工智能 2026-07-08 6
摄影 2023-10-02 3

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

2026-07-08 21:39:46

0. 总览

这是我和 AI 智能体共同探讨写作的一篇博文,用来帮助梳理大语言模型推理服务的相关知识与实践。这篇文章的主线是:一个开源模型如何从社区里的文件,变成服务器上可被业务调用的 LLM API 服务。这条链路看似只是“下载模型、启动服务、发请求”,实际涉及模型文件格式、GPU 显存管理、请求调度、KV cache、并发、量化、性能指标等多个层次。

这些问题琐碎、复杂,又各有各的重要。这让我想到了在“上古”时代,大家热衷于探索计算机中“从加电到系统启动”、“从 fork 到子进程执行”、“从输入一个 URL 到浏览器完成页面渲染”等水面以下鲜为人知的过程。

庐山烟雨浙江潮。在人类把越来越多的工作——交付也好、让位也罢——给 AI 智能体的当下和未来,愿我们能始终保留探索、理解和表达的渴望。

0.1 什么是 LLM inference

LLM inference 指的是:给一个已经训练好的语言模型输入一段文本,让它继续预测后续 token 的过程。它和 training 的区别在于,inference 不再更新模型参数,只做前向计算;training 则要计算损失、反向传播并更新权重。

以“北京是中国的”这个 prompt 为例,模型不会一次性“写出答案”,而是做下一个 token 预测:

输入:北京是中国的
预测下一个 token:首
输入:北京是中国的首
预测下一个 token:都
输入:北京是中国的首都
预测下一个 token:。

这就是自回归生成(autoregressive generation)。绝大多数文本 LLM 都基于 Transformer 架构中的 causal decoder:每个位置只能看见自己之前的 token,因此天然适合“根据上文预测下一个 token”。

0.2 inference/serving/chat completion/generation 分别指什么

这几个词经常混用,但它们处在不同抽象层:

术语 关注点 简单理解
inference 模型计算 把 token 喂给模型,得到下一个 token 的概率分布
generation 生成算法 根据概率分布和采样策略,决定真正输出哪个 token
serving 工程系统 把模型包装成可并发访问、可监控、可扩缩容的服务
chat completion API 形态 接收 system/user/assistant 消息,返回 assistant 回复

举个例子:temperature=0.7top_p=0.9 属于 generation;/v1/chat/completions 属于 API;batching、排队、限流、metrics 属于 serving;矩阵乘法、attention、KV cache 属于 inference 内部计算。

现代开源 serving 框架通常会兼容 OpenAI Chat Completions API,这样上层应用可以用同一套 SDK 切换云模型和自部署模型。

0.3 模型从社区到 API 服务经历哪些环节

一个典型 lifecycle 可以拆成下面几步:

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["监控与压测"]

其中最容易被低估的是中间三步:加载、分配、调度。一个模型能否跑起来,首先取决于权重和 KV cache 能否放进显存;一个服务能否跑得好,则取决于调度器能否在吞吐和延迟之间做合理权衡。

例如,同样是 8B 模型,单人本地聊天时可能很流畅;一旦放到线上同时接收 100 个长上下文请求,瓶颈就可能从“模型权重放不下”变成“KV cache 被并发请求撑爆”。

0.4 vLLM/Ollama/SGLang/TGI/llama.cpp 分别解决什么问题

这些工具都能“跑模型”,但设计目标不同:

工具 更适合的场景 核心关键词
vLLM GPU 服务端高吞吐推理 PagedAttention、continuous batching、OpenAI-compatible server
Ollama 本地开发、个人电脑、快速体验 模型管理、Modelfile、本地 REST API、GGUF
SGLang 复杂推理程序、高性能服务、结构化输出 RadixAttention、structured generation、OpenAI-compatible API
TGI Hugging Face 生态中的生产化 serving Rust router、streaming、metrics、HF 集成
llama.cpp CPU/端侧/消费级设备 C/C++、GGUF、低比特量化、本地运行

截至 2026 年 7 月,Hugging Face 的 Text Generation Inference 仓库已在 2026-03-21 归档并进入 maintenance mode;它仍然值得学习,因为很多 serving 概念如 continuous batching、SSE streaming、Prometheus metrics 都非常典型,但新项目选型通常会优先看 vLLM、SGLang 或专用后端。

1. 模型来源与命名

1.1 开源模型通常发布在哪里

开源或 open-weight 模型通常发布在以下地方:

  • Hugging Face Model Hub:目前最主流的模型托管平台,模型通常以一个 Git 仓库形式存在,包含权重、配置、tokenizer、model card 等。
  • ModelScope:国内常用模型社区,访问和下载速度对国内服务器更友好。
  • Ollama Library:面向 Ollama 的模型分发入口,用户可以用 ollama pull qwen2.5 这类命令直接拉取。
  • 项目方官网或 GitHub release:例如 llama.cpp 生态中的 GGUF 文件经常由社区作者转换后发布在 Hugging Face。

Hugging Face 上的模型名一般是 组织名/仓库名,例如 Qwen/Qwen2.5-Coder-7B-Instruct。这个名字不是随便起的,它同时传达了模型家族、版本、能力方向、规模和训练阶段。

1.2 模型名字怎么看

Qwen/Qwen2.5-Coder-7B-Instruct 为例,可以拆成几段:

Qwen / Qwen2.5 - Coder - 7B - Instruct
组织   家族版本   能力方向 参数规模  指令微调版本

常见字段含义如下:

字段 含义 例子
模型家族 基础架构和训练路线 Llama、Qwen、Mistral、Gemma、DeepSeek
版本号 同一家族的迭代 Llama 3.1、Qwen2.5、Mistral v0.3
参数规模 参数数量近似值 7B、8B、14B、32B、70B
能力方向 训练数据或任务偏向 Coder、Math、Vision、Reasoning
训练阶段 是否面向对话和指令 Base、Chat、Instruct
精度/量化 权重存储格式 BF16、FP16、INT8、AWQ、GPTQ、GGUF

Base 模型像“只读了大量书但没学会按人类指令办事的学生”;InstructChat 模型则经过指令微调或偏好对齐,更像“知道如何回答问题的助教”。做服务时,如果目标是聊天、问答、代码助手,通常优先选 Instruct/Chat;如果要继续训练、做领域适配或研究模型行为,Base 更合适。

CoderMathVisionReasoning 则描述能力方向,不是训练阶段。比如 Qwen2.5-Coder 这类模型通常在代码数据、单文件补全、debug、仓库级代码理解、工具调用等任务上做了更强训练;它仍然可以同时是 Instruct 模型。也就是说,Coder 回答“擅长什么”,Instruct 回答“是否学会按指令对话”。

8B 表示大约 80 亿参数。参数越多通常能力越强,但显存、延迟、吞吐成本也更高。粗略估算,BF16/FP16 每个参数 2 字节,8B 权重仅权重就约 16 GB;70B 则约 140 GB,还不包括 KV cache 和 runtime buffer。

还有一类越来越常见的命名是 35B-A3B235B-A22B。这里的 A 通常表示 activated parameters,常见于 Mixture-of-Experts(MoE)模型。例如官方 Qwen3.5-35B-A3B model card 标注为 35B total、3B activated:总参数约 35B,但每个 token 只路由到其中一部分专家,实际参与该 token 计算的参数约 3B。注意这不等于显存只需要装 3B 参数;如果不做专家 offload 或分布式切分,服务端仍然要管理完整专家权重。

1.3 选择模型前要问什么

选模型不是“越大越好”,而是把任务、硬件、性能目标三者对齐。可以先问五个问题:

  1. 任务是什么:聊天、代码、RAG、摘要、分类、agent、批量离线生成,还是 embedding?
  2. 输入和输出多长:短问答和 128K 长上下文 RAG 对系统的压力完全不同。
  3. 硬件是什么:单张 24 GB 消费卡、A100 80 GB、H100、多机多卡,还是 CPU/Apple Silicon?
  4. 服务目标是什么:更在意 TTFT、输出吞吐、P99 延迟、成本,还是质量?
  5. 框架是否支持:模型架构、量化格式、chat template、context length 是否被 vLLM/SGLang/Ollama 支持?

一个实际选择例子:如果要在一张 24 GB GPU 上搭一个代码助手,可以先考虑 7B/8B 级别的 Qwen2.5-Coder 或同级模型;如果是公司内部 RAG 问答,可能更关注长上下文、中文能力和吞吐;如果是本地离线使用,Ollama + GGUF 量化模型可能比完整服务端框架更省心。

1.4 上下文窗口怎么看

上下文窗口(context window)是模型一次生成时能“看见”的 token 总预算,通常包括输入 prompt、历史对话、工具返回、RAG 检索片段,以及本次将要生成的输出。一个 128K context 模型并不是说可以输入 128K token 后再无限输出,而是输入和输出共享这块预算。

$$ N_{\text{input}} + N_{\text{output}} \leq N_{\text{context}} $$

某个模型的上下文窗口为什么是某个值,通常由三层因素共同决定。第一层是训练时见过的位置范围:模型在预训练或后训练中通常只在某个最大长度内学习位置关系,这会体现在 config.jsonmax_position_embeddingsmax_model_len 或类似字段里。第二层是位置编码机制:现代 LLM 多用 RoPE 这类相对位置编码,能通过 RoPE scaling、YaRN 等方法把原生窗口外推到更长范围,但外推越远,质量越需要实测验证。第三层是推理系统限制:即使模型理论上支持 128K,serving 框架也必须能分配足够 KV cache,并在可接受的 TTFT、ITL 和显存成本下跑起来。

因此,model card 上的上下文窗口不是一个纯数学常数,而更像“训练设定 + 架构设计 + 工程验证”共同给出的服务承诺。比如一个模型原生训练到 32K,通过 YaRN 外推到 128K,含义是它在配置和验证上支持 128K;但如果线上请求几乎都是短 prompt,强行把 max_model_len 开到最大,反而可能浪费 KV cache 预算。

例如 Qwen3.5-35B-A3B 的 model card 标注了 262,144 token 原生上下文,并可扩展到约 1,010,000 token。这个数字说明 runtime 可以配置很长的 max_model_len,但实际能否线上使用,还要看显存、TTFT、KV cache 和请求长度分布。

可以把上下文窗口想成考试时允许带进考场的一摞资料。窗口越大,能带的资料越多;但资料越厚,读题(prefill)越慢,便签(KV cache)越占显存。长上下文服务的难点不是“模型宣称支持 128K”,而是在线上并发里让很多用户同时使用长上下文时仍然满足延迟和成本目标。

“能放进上下文”也不等于“模型能稳定用好”。超长上下文可能出现检索位置偏置、注意力稀释、无关信息干扰等问题。因此工程上常配合上下文压缩:

  • 摘要压缩:把历史对话或长文档先压成短摘要,牺牲细节换取空间。
  • RAG 检索:只把最相关片段放进 prompt,而不是把整库塞进去。
  • 滑动窗口:保留最近若干轮对话,丢弃较早内容。
  • 工具结果裁剪:对搜索、日志、网页结果做截断、去重、重排。
  • 长上下文外推:用 RoPE scaling 等方法扩展位置编码,例如 Qwen3 文档中提到的 YaRN

上下文压缩和 KV cache 优化是两类不同问题:前者减少“要读的内容”,后者降低“读过内容的缓存成本”。例如 prefix cache 没有压缩语义信息,只是复用相同前缀的计算结果;RAG 则是真的改变了进入模型的文本。

2. 模型文件结构

2.1 模型目录里有哪些文件

一个 Hugging Face 风格的 causal LM 目录通常长这样:

model/
  config.json
  generation_config.json
  tokenizer.json
  tokenizer_config.json
  special_tokens_map.json
  model.safetensors
  model-00001-of-00004.safetensors
  model-00002-of-00004.safetensors
  model.safetensors.index.json
  README.md

各类文件的职责可以这样理解:

文件 作用
config.json 描述模型结构,例如层数、hidden size、attention heads、context length
*.safetensors / *.bin 真正的模型权重,也就是大量矩阵参数
tokenizer.json 文本和 token id 之间的转换规则
tokenizer_config.json tokenizer 的附加配置,常包含 chat template
generation_config.json 默认生成参数,例如 temperature、top_p、eos token
README.md model card,说明训练、用途、限制、license、示例

可以把模型目录理解成一个可执行程序的“安装包”:权重是主体二进制,config 是硬件说明书,tokenizer 是输入法,model card 是用户手册。

2.2 权重文件是什么

权重文件存储的是神经网络中所有可学习参数。在线性层里,它们通常是矩阵;在 embedding 层里,它们像一张 token id 到向量的查表;在 attention 和 MLP 中,它们决定了模型如何把上下文变换成下一个 token 的 logits。

logits 可以理解成模型给词表里每个候选 token 打的“未归一化分数”。例如 prompt 是“北京是中国的”,模型可能给 城市 等候选 token 分别打分;这些分数经过 softmax 才会变成概率,生成策略再根据概率选择下一个 token。因此,模型前向计算本身只是产出分数分布,真正输出哪个 token 还取决于 temperature、top-p、beam search 等解码策略。

现代模型常用 safetensors 格式,因为它避免了 Python pickle 反序列化的安全问题,并支持更高效的张量加载。大模型权重经常被切成多个 shard,例如 model-00001-of-00008.safetensors,原因很朴素:单个文件太大,不便于下载、校验和并行加载。

一个容易混淆的点是:权重文件不是模型代码。真正执行前向计算的是 PyTorch、Transformers、vLLM、SGLang 或 llama.cpp 中的模型实现;权重文件只是把这些实现中的矩阵填上具体数值。也就是说,config.json 告诉 runtime“这是一栋 32 层楼的建筑”,权重文件告诉 runtime“每层楼具体摆了哪些钢筋水泥”。

2.3 tokenizer 是什么

tokenizer 负责把人类文本转换成模型能处理的整数序列。模型并不直接理解“北京”或“GPU”,它看到的是 token id,例如:

文本:北京是中国的首都
token:["北京", "是", "中国", "的", "首都"]
id:  [xxxxx, xxxx, xxxxx, xxx, xxxxx]

实际切分不一定符合人类词语边界。英文中 unbelievable 可能被拆成 unbelievable;中文中一个词也可能被拆成多个 token。tokenizer 的差异会直接影响:

  • 同一段文本算多少 token,从而影响上下文长度和计费。
  • prompt 是否按模型训练时的格式组织。
  • stop token、BOS/EOS、role token 是否正确。

聊天模型还依赖 chat template。例如同样的消息:

user: 你好
assistant: 你好,有什么可以帮你?

不同模型可能需要被格式化成 [INST]...[/INST]<|user|>...<|assistant|> 或其他控制 token。如果 chat template 用错,模型就像看到一份格式混乱的试卷,能力会明显下降。

2.4 模型配置决定什么

config.json 决定 runtime 如何搭建模型结构。几个常见字段:

配置 含义 影响
num_hidden_layers Transformer 层数 影响计算量和权重大小
hidden_size 每个 token 的隐藏向量维度 影响矩阵乘法规模
num_attention_heads Query attention heads 数量 影响 attention 并行结构
num_key_value_heads KV heads 数量 影响 KV cache 大小,GQA/MQA 会减少它
max_position_embeddings 原生上下文长度 影响最多能看多长输入
rope_scaling RoPE 外推配置 影响长上下文扩展
torch_dtype 默认精度 影响显存占用和硬件兼容

例如 Qwen2.5-Coder-7B-Instruct 的 model card 会列出层数、attention heads、KV heads 和 context length。对 serving 工程来说,num_key_value_heads 特别关键:它决定每个 token 要缓存多少 K/V 向量,进而影响高并发长上下文时的显存压力。

3. 模型本身架构

前面看到的 config.json 和权重文件,合在一起描述的正是一个神经网络。绝大多数文本 LLM 都是 decoder-only Transformer:token 先变成向量,依次穿过许多相似的 Transformer block,最后得到词表上每个候选 token 的 logits。模型“有多少层”“有多宽”“有多少个 attention/KV heads”“是否采用 MoE”,说的都是这条计算路径的不同设计。

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

图中的 block 会堆叠几十层甚至上百层。每层都保留一条 residual connection(残差连接),让新计算结果加回原来的隐藏状态;RMSNorm、LayerNorm 等归一化层则帮助数值保持稳定。不同模型的具体排列会有差异,例如 norm 放在子层前还是后、是否共享输入和输出 embedding,但不改变“attention 负责从上下文取信息,MLP/MoE 负责逐 token 变换”的主干。

3.1 Causal self-attention:模型如何读取上文

对某一层的每个 token 向量,attention 会分别线性投影出 Query(Q)、Key(K)和 Value(V)。直觉上,当前 token 用 Q 提出“我需要什么信息”,用它和历史 token 的 K 计算相关性,再按照权重汇总对应的 V:

$$ \mathbf{Q} = \mathbf{X}\mathbf{W}_Q, \qquad \mathbf{K} = \mathbf{X}\mathbf{W}_K, \qquad \mathbf{V} = \mathbf{X}\mathbf{W}_V $$

$$ \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} $$

其中,$\mathbf{X}$ 是输入隐藏状态,$d_h$ 是单个 head 的维度,$\mathbf{M}_{\mathrm{causal}}$ 是 causal mask:它把当前位置右侧的 attention score 置为负无穷,因此“北京是中国的”在预测“首”时只能读取前面的 token,不能偷看尚未生成的“都”。这就是 causal decoder 能自回归生成的原因。

多头注意力(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 很重要;这也解释了为什么 num_key_value_heads 往往小于 num_attention_heads

注意力不是“把整段文本一次读懂”的魔法。每一层都只是在当前表示上做一次加权信息交换;层数、训练数据和参数共同决定这种交换最终能形成多强的语言与推理能力。从推理成本看,prefill 时需要处理 prompt 内 token 两两之间的注意力关系,decode 时每个新 token 则要查询不断增长的历史 K/V,这正是后文 Prefill、Decode 和 KV cache 优化的计算基础。

3.2 MLP:每个 token 的特征变换

attention 让 token 之间交换信息;attention 之后的 MLP(也叫 FFN,feed-forward network)主要对每个 token 自己的隐藏向量做非线性变换。一个简化的两层 MLP 可以写成:

$$ \mathbf{h}^{\prime} = \mathbf{W}_{\mathrm{down}}\sigma\left(\mathbf{W}_{\mathrm{up}}\mathbf{h}\right) $$

其中 W_up 通常先把 hidden_size 扩展到更大的 intermediate_size,激活函数(如 SiLU/GELU)引入非线性,W_down 再投影回原维度。很多现代模型使用 SwiGLU 等门控变体:它额外计算一条 gate 分支,选择性地放大或抑制特征。MLP 往往占模型参数中的很大比例,也往往是单层矩阵乘法计算的重要来源。

因此可以把一个 dense Transformer block 粗略记成两步:先用 attention 问“上下文里哪些信息与我有关”,再用 MLP 问“结合这些信息后,应怎样更新这个 token 的表示”。MoE 的关键变化正发生在第二步:它不再让所有 token 经过同一个 MLP,而是让路由器为每个 token 选择少数几个专家 MLP。

3.3 MoE 模型架构:稀疏激活的专家网络

MoE(Mixture of Experts)通常保留 attention 子层不变,把 dense MLP 替换为一组并列的 expert MLP,并新增一个 router(也称 gate)。router 根据当前 token 的隐藏状态,为所有专家打分,选出 top-k 个专家;token 只经过被选中的专家,最后按路由权重合并结果。

对隐藏状态 $\mathbf{h}$,一个简化的 top-$k$ 路由过程可写成:

$$ \mathbf{p} = \operatorname{softmax}(\mathbf{W}_{\mathrm{router}}\mathbf{h}) $$

$$ \mathcal{S} = \operatorname{TopK}(\mathbf{p}, k) $$

$$ \operatorname{MoE}(\mathbf{h}) = \sum_{i \in \mathcal{S}} \tilde{p}_i E_i(\mathbf{h}) $$

这里 $E_i$ 是第 $i$ 个 expert MLP,$\tilde{p}_i$ 是在被选中专家上重新归一化后的路由权重。不同模型会在 router 打分、归一化和共享专家的处理上有所差异,但“只激活少数专家”的原则相同。

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"]

这带来一个重要区分:总参数(total parameters)包含所有专家,而激活参数(activated parameters)只计算当前 token 实际经过的共享部分和 top-k 专家。例如“35B-A3B”可以理解为模型总共约 35B 参数,但单个 token 的一次前向计算只会激活约 3B 参数量级。它让模型能拥有更大的总容量,同时把每个 token 的计算量控制在接近较小 dense 模型的水平。

但 MoE 并不是“35B 模型只需要 3B 的显存”。正常部署时,router 可能把不同 token 路由到任意专家,所以完整专家权重通常仍需在 GPU 或整个多卡集群中可访问;总参数仍决定权重存储、下载和冷启动成本。MoE 的优势主要是降低每个 token 的 FLOPs,不会等比例降低权重显存,也不会消除 KV cache 的开销。

MoE serving 还多了几项工程特性:

  • 负载均衡:若大量 token 都选择同一专家,该专家会成为热点;训练会加入负载均衡目标,runtime 也要尽量让批次中的路由高效执行。
  • capacity 与丢弃/回退:一个专家在一个 batch 内能处理的 token 数有限。超过 capacity 的 token 需要重新路由、跳过或采用实现定义的回退策略;不同模型和 runtime 的行为并不完全相同。
  • 专家并行:专家可分布在不同 GPU 上。这样能分摊权重显存,却会引入 token dispatch 和 all-to-all 通信;GPU 拓扑与通信带宽会直接影响吞吐和延迟。
  • 共享专家与细粒度专家:一些新模型会增加所有 token 都经过的 shared expert,或把 MLP 切成更多、更小的专家。它们属于具体架构选择,部署时应以 model card 和 runtime 支持矩阵为准。

选择 MoE 模型时,除了看 total/activated parameters,还应确认 runtime 是否支持该模型的 router、专家数和并行方式,并在真实请求长度和并发下压测。短 batch、跨 GPU 通信较慢或专家路由高度不均衡时,MoE 的实际收益可能低于参数命名带来的直觉;反过来,在足够大的 batch 和合适的 expert parallel 配置下,它能提供很有吸引力的质量—计算量折中。

4. 模型加载与运行

4.1 启动服务时发生了什么

当执行类似 vllm serve Qwen/Qwen2.5-Coder-7B-Instruct 时,系统大致会做这些事:

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["等待请求"]

这里有两个时间概念:冷启动时间请求延迟。冷启动是服务从 0 到 ready 的时间,主要花在下载、磁盘读取、权重加载、kernel 初始化;请求延迟则是服务 ready 后处理每个请求的时间。一个 70B 模型冷启动可能需要数分钟,但不代表每个请求都要等数分钟。

这里的 kernel 指的是 GPU kernel,不是 Linux/macOS 的 system kernel。按照 CUDA Programming Guide 的语境,kernel 是运行在 GPU 上的一段并行计算函数,例如矩阵乘法、attention、RMSNorm、采样前的 logits 处理等。推理框架会尽量使用高度优化的 kernel,减少 Python 调度和显存读写开销。

模型加载失败的常见原因也和这些步骤对应:模型架构 runtime 不支持、权重格式不支持、显存不够、CUDA/driver/PyTorch 版本不匹配、gated model 没有 token 权限等。

4.2 显存主要被谁占用

LLM serving 的显存通常被四类东西占用:

  1. 模型权重:近似 参数量 × 每参数字节数。8B BF16 约 16 GB,8B INT4 约 4 GB 再加 scale 等元数据。
  2. KV cache:随 batch size、上下文长度、生成长度线性增长,长上下文和高并发时可能超过权重本身。
  3. activation 和 workspace:前向计算中的临时张量、CUDA graph、attention kernel 工作区等。
  4. runtime buffer:调度器、通信 buffer、tokenizer/CPU buffer、多卡通信等开销。

KV cache 的粗略估算公式是:

$$ M_{\mathrm{KV}} \approx 2 \times L \times H_{\mathrm{KV}} \times d_h \times T \times B \times b $$

其中 $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 倍。

可以把权重看成餐厅固定装修,KV cache 看成每桌客人正在使用的餐具和便签。装修开店时就占空间;客人越多、点单越长,桌面上的临时物品越多。

4.3 单卡和多卡怎么跑

单卡部署最简单:整个模型权重、KV cache 和计算都在一张 GPU 上。多卡部署的目标是解决两个问题:单卡放不下单卡算不快

常见并行方式如下:

并行方式 做法 适合场景
Tensor Parallelism 把一个矩阵切到多张 GPU 上一起算 大模型单层太大,常见于 vLLM/SGLang
Pipeline Parallelism 把不同层放到不同 GPU 上,像流水线一样传递 模型层数多、跨节点部署
Data Parallelism 多个副本各自处理不同请求 单副本能放下,但需要更高 QPS
Expert Parallelism MoE 模型中把专家分散到不同 GPU DeepSeek-V3、Mixtral 等 MoE 模型

vLLM parallelism 文档SGLang 文档 都覆盖了这些部署形态。实践中,单机多卡优先考虑 tensor parallel;如果模型已经能单卡放下而只是 QPS 不够,多个副本加负载均衡往往更简单。

4.4 推理硬件基础

CPU 也能跑 LLM,尤其配合 llama.cpp 和 GGUF 量化模型时,本地实验很方便。但 CPU 的并行矩阵计算能力和内存带宽通常远弱于服务器 GPU,所以它更适合小模型、低并发、离线或端侧场景。GPU 推理的优势在于:大量矩阵乘法可以并行执行,显存带宽高,Tensor Core 能加速 FP16/BF16/FP8/INT8 等低精度计算。

一个 GPU 可以粗略拆成几类部件:

部件 作用 对 LLM 推理的影响
HBM/GDDR 显存 存权重、KV cache、临时张量 决定模型和并发能不能放下
显存带宽 权重和 KV cache 每秒能搬多少数据 decode 阶段经常卡在这里
SM GPU 的主要计算单元 执行大量并行线程
Tensor Core 专门加速矩阵乘法 对 FP16/BF16/FP8/INT8 推理很关键
NVLink/PCIe GPU 间或 CPU-GPU 间通信 影响多卡 tensor parallel 和 KV 传输

实际运行时,CPU 负责发起 kernel launch,GPU 在大量 SM 上并行执行 kernel。矩阵乘法会被切成许多 tile,Tensor Core 对这些小块做高吞吐乘加;attention kernel 则要在 Q/K/V、mask、softmax、输出投影之间组织计算和显存访问。很多推理优化的本质,不是“换一个数学公式”,而是减少不必要的数据搬运、kernel launch 和中间张量写回。

常见硬件可以按定位理解:

GPU 常见定位 直观理解
RTX 4090/5090 消费级开发、单机实验 便宜但显存和多卡互联受限
L20 国内云上常见推理卡 适合中小模型和成本敏感推理
L40S 数据中心推理、图形和多模态 48GB 显存,兼顾 AI 与图形负载
A100 上一代训练和推理主力 40/80GB 显存,生态成熟
H100 / H200 高端训练和推理 HBM 带宽高,FP8 和 Transformer Engine 友好
B200/GB200 Blackwell 代大规模 AI 集群 面向更大模型、更高吞吐和更低 token 成本

对 LLM serving 来说,GPU 型号不能只看 FLOPS。短 prompt 大 batch 的 prefill 更吃算力,长输出 decode 更吃显存带宽,长上下文高并发更吃显存容量和 KV cache 管理。选择硬件时,应同时看显存容量、显存带宽、低精度支持、互联方式和实际租赁成本。

5. 请求处理流程:Prefill 与 Decode

5.1 请求进入服务后经历哪些阶段

先看一个常见 LLM serving 架构。不同框架命名不同,但组件职责大体类似:

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

其中 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 则用于压测和线上诊断。

一次在线请求从客户端到模型输出,通常经历:

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

注意,客户端感受到的延迟不只是 GPU 计算时间,还包括网络、HTTP 处理、tokenization、排队、detokenization 和流式传输。压测时如果只看 GPU kernel 时间,会低估真实服务延迟。

5.2 Prefill 阶段要理解什么

Prefill 是模型“读题”的阶段:服务把完整 prompt 一次性送入模型,计算所有输入 token 的 hidden states,并为后续生成建立 KV cache。因为 prompt 中的 token 已经全部知道,prefill 可以高度并行,所以它通常更偏 compute-bound。

例子:

prompt: 请总结下面这份 4000 token 的报告...
prefill: 一次性处理这 4000 个输入 token,建立 KV cache,并产生第一个输出 token

Prefill 直接决定 TTFT(Time To First Token)的下限。prompt 越长,模型“读题”越久;如果前面还有其他请求排队,TTFT 还会继续上升。

一些系统优化也发生在 prefill 附近。例如 vLLM 的 Automatic Prefix Caching 会复用相同前缀的 KV cache;这对 RAG、长系统 prompt、多轮对话很有价值。类比一下:如果每个学生都交同一份试卷前半部分,老师没必要每次重新读一遍公共题干。

5.3 Decode 阶段要理解什么

Decode 是模型“逐字答题”的阶段。自回归模型必须等上一个 token 出来后,才能把它追加到上下文里预测下一个 token,因此 decode 在单条序列内部是串行的。

第 1 步:prompt -> 北
第 2 步:prompt + 北 -> 京
第 3 步:prompt + 北京 -> 是

Decode 阶段有两个关键事实:

  • 每生成一个 token 都要读一遍模型权重,所以它常常 memory-bound。
  • KV cache 避免了重复计算历史 token,但每步仍要读取历史 K/V 来做 attention。

为什么 batching 能提升吞吐?假设服务同时有 16 个请求都在 decode。GPU 读取一次权重,可以顺便为 16 个请求各生成一个 token。单个请求未必更快,但系统总输出 token/s 会变高。

这也是 latency-throughput trade-off 的来源:batch 大,GPU 利用率高,吞吐好;但请求可能等 batch、等调度、和其他请求共享资源,单请求延迟变差。

5.4 生成策略属于哪里

模型前向计算输出的是 logits,也就是每个 token 的分数。生成策略负责把分数变成真正输出的 token。

常见参数:

参数 作用 直观理解
temperature 调整分布尖锐程度 越低越保守,越高越发散
top_p 只在累计概率前 p 的 token 中采样 从“高概率候选集合”里抽
top_k 只保留概率最高的 k 个 token 限制候选菜单大小
max_tokens 最多生成多少 token 防止输出无限变长
stop 遇到指定字符串/token 停止 例如遇到 </answer> 停止
repetition_penalty 惩罚重复 token 减少复读

如果 temperature=0 或接近 0,模型更像做选择题,总选最高分答案;如果 temperature 较高,则更像写作时保留更多表达可能。Beam search 会在每一步保留多个候选序列,适合翻译、图像描述这类输入约束较强的任务,但开放式聊天里可能更容易产生保守、重复的回答。工程上,生成参数不仅影响质量,也影响性能:max_tokens 越大,最坏 E2E latency 越高;beam search、parallel sampling 会增加每个请求的计算和 KV cache 压力。

什么时候停止生成也很重要。最常见的停止条件有四种:

  • 模型生成 EOS token。EOS 是 End Of Sequence,表示模型认为序列结束。
  • 达到 max_tokensmax_new_tokens 上限。
  • 命中用户配置的 stop string,例如生成到 </answer>
  • 客户端取消、服务端 timeout,或调度器因资源压力中断请求。

EOS 是 tokenizer 词表里的特殊 token,stop string 则是服务层按文本匹配的停止规则。GenerationConfig 里常见的 eos_token_idforced_eos_token_idstop_strings 就对应这些控制。二者经常一起用:EOS 让模型自然结束,stop string 用来适配业务协议或结构化输出。

还有一类优化叫 speculative decoding:让小模型先草拟多个 token,大模型一次性验证。类比写论文时让助研先打草稿,导师只负责批改和确认。如果草稿命中率高,decode 可以明显加速。

5.5 什么是 PD 分离

PD 分离(prefill-decode disaggregation)指把 prefill 和 decode 放到不同 worker、不同 GPU,甚至不同机器上执行。动机很直接:prefill 更偏 compute-bound,decode 更偏 memory-bound;把它们混在同一批 GPU 上时,长 prompt 的 prefill 可能打断正在 decode 的请求,拉高 ITL,而 decode 的高频小步计算也会影响 prefill 调度。

可以把它想成餐厅里把“读菜单和备菜”和“逐道上菜”拆成两个工位。前者适合大块时间集中处理,后者需要稳定、频繁、低抖动地出菜。拆开后,每个工位能按自己的瓶颈配置资源。

研究系统 SplitwiseDistServe 都讨论了这种 phase splitting / disaggregation 思路。收益是降低 prefill 和 decode 的相互干扰,并可分别优化 TTFT 和 TPOT;代价是系统复杂度更高,需要在阶段之间传输 KV cache 或中间状态,对网络、调度器和容错都有更高要求。小规模单机部署通常不必一开始就做 PD 分离,但长上下文、高并发、严格 SLO 的生产集群会越来越关注它。

6. Serving 框架对比

6.1 vLLM

vLLM 的核心目标是 GPU 服务端高吞吐推理。它最出名的设计是 PagedAttention:借鉴操作系统虚拟内存分页思想,把 KV cache 分块管理,减少连续大块显存分配带来的碎片和浪费。

直观地说,传统 KV cache 管理像给每个请求预留一整排连续座位;如果客人中途离开或增加,座位容易碎片化。PagedAttention 像把座位拆成小块,按需分配、可复用、可共享,从而容纳更多并发请求。

vLLM 适合:

  • 部署 Hugging Face 上的主流 open-weight 模型。
  • 提供 OpenAI-compatible API。
  • 追求较高吞吐和较成熟的生产部署能力。
  • 需要量化、多卡、prefix caching、benchmark 工具链。

它不只是“模型 runner”,而是一个面向 serving 的系统:调度、batching、KV cache 管理、metrics、并行策略都很重要。学习 vLLM,重点不是背启动命令,而是理解它为什么能在同等显存下塞进更多有效请求。

6.2 Ollama

Ollama 更像“本地模型运行器 + 模型管理工具”。它降低了本地跑模型的门槛:ollama run llama3.1 就能拉模型、启动 runtime、进入对话。它默认提供本地 API,官方文档中默认 base URL 是 http://localhost:11434/api,并支持 OpenAI compatibility

Ollama 的一个重要概念是 Modelfile,类似 Dockerfile:用 FROM 指定基础模型,用 SYSTEM 写系统提示词,用 PARAMETER 固定 temperature、context length 等运行参数。

它适合:

  • 个人电脑、本地开发、离线演示。
  • 快速比较不同模型。
  • 把 GGUF 模型包装成简单 API。
  • 对极致吞吐要求不高,但希望易用。

Ollama 通常不是大规模线上 GPU serving 的首选,但非常适合建立直觉:模型如何被拉取、加载、运行、暴露 API,以及量化模型在本地硬件上如何权衡速度和质量。

6.3 SGLang

SGLang 的定位是高性能 serving 和复杂 LLM program 执行。它不只关心“单次 chat completion”,还关心多轮调用、结构化输出、分支控制、工具调用、agent workflow 等更复杂的推理程序。

其论文 SGLang: Efficient Execution of Structured Language Model Programs 提出了前端语言和后端 runtime 的组合;后端中的 RadixAttention 用于复用共享前缀的 KV cache。对多轮对话、few-shot prompt、RAG 模板这类“很多请求共享长前缀”的工作负载,这类优化很关键。

可以把 SGLang 想成“懂得复用草稿纸的考试系统”:如果 100 道题都共享同一段材料,它不会让模型每次从头读材料,而是尽量复用已经算过的前缀状态。

它适合:

  • 结构化输出,如 JSON、regex、schema-constrained generation。
  • agent、多步推理、复杂 prompt program。
  • 高吞吐、低延迟、多卡 serving。
  • 需要快速跟进新模型和 reasoning parser 的场景。

6.4 其他相关框架

除了 vLLM、Ollama、SGLang,还常见以下系统:

  • llama.cpp:C/C++ 实现,主打 GGUF、CPU/Metal/CUDA/端侧推理,是很多本地工具的底座。
  • TensorRT-LLM:NVIDIA 面向高性能推理的优化库,适合愿意为特定硬件做更深优化的生产场景。
  • Triton Inference Server:通用推理服务框架,可服务多种模型后端,不只 LLM。
  • LMDeploy:国内常见的 LLM 部署工具链,支持 TurboMind、量化和服务化。
  • Text Generation Inference:Hugging Face 早期生产 serving 代表,现在更适合作为历史和架构参考。

选型时可以先问:我要的是本地易用、服务端吞吐、复杂程序执行、硬件极致优化,还是通用推理平台?不同答案会指向不同工具。

7. 并发、Batch 与调度

7.1 什么是并发

“并发”在 LLM serving 里至少有四种口径:

口径 含义
客户端并发 同时有多少客户端连接或请求在发起
排队并发 有多少请求已经进入系统但还没完成
running sequences 有多少序列正在被调度器处理
batch size / batched tokens 某次 GPU forward 实际合并了多少序列或 token

用户说“并发 100”,可能只是压测工具同时开 100 个连接;但 GPU 看到的可能是某一时刻 batch 里有 32 条 decode 序列,另有 68 条排队或等待 prefill。分析问题时必须先明确口径。

一个小例子:咖啡店门口有 100 人排队,不代表咖啡机同时在做 100 杯。真正同时制作的杯数取决于咖啡机容量、店员调度、每杯饮品复杂度和是否有人点超大杯。

7.2 并发和吞吐是什么关系

并发升高通常会先提升吞吐,因为 GPU 从“小批量低利用率”变成“更大 batch、更高利用率”。但超过某个点后,吞吐会趋于饱和,延迟开始快速上升。

可以用一个简化关系理解:

$$ \overline{L} = \lambda,\overline{W} $$

这就是 Little’s Law 在服务系统中的直觉版本:$\overline{L}$ 是系统内平均未完成请求数,$\lambda$ 是稳定到达率(例如 QPS),$\overline{W}$ 是平均端到端延迟。假设服务稳定处理 10 QPS,每个请求平均 5 秒完成,那么系统里平均大约有 50 个请求处于未完成状态。

LLM 的特殊性在于,不同请求的“大小”差异很大:一个 50 token 输入、20 token 输出的请求,和一个 8000 token 输入、2000 token 输出的请求,对 GPU 的占用完全不是一个量级。因此只说 QPS 不够,必须同时说 input/output token 分布。

7.3 Serving 调度器要解决什么

LLM serving 调度器的任务是把请求组织成 GPU 能高效处理的批次,同时尽量控制延迟。它要处理几类矛盾:

  • prefill 计算重,decode 更频繁但每步较小。
  • 新请求希望尽快 prefill 以降低 TTFT,老请求希望 decode 连续进行以降低 ITL。
  • batch 大有利于吞吐,但会增加等待和干扰。
  • 长请求占用 KV cache 久,短请求又不应该被饿死。

continuous batching 的思想是:不要等一整个固定 batch 全部完成后再接新请求,而是在每个 decode step 动态加入新请求、移除完成请求。这比传统静态 batching 更适合 LLM,因为每个请求输出长度不同。

chunked prefill 则是把很长的 prefill 切成块,避免一个超长 prompt 长时间占住 GPU,导致正在 decode 的请求 token 间隔变大。vLLM 的 optimization 文档 中就把 chunked prefill 作为调优点之一。

7.4 什么是容量规划

容量规划回答的问题是:在给定模型、硬件和 SLO 下,服务最多能承载多少负载。SLO 是 Service Level Objective,即服务质量目标,例如 P99 TTFT < 1sP99 ITL < 80ms、错误率低于某个阈值。它不是“系统极限能跑多快”,而是“在用户体验可接受的条件下能跑多大负载”。

正确的压测方式不是只问“最大 QPS 是多少”,而是固定场景:

模型:Qwen2.5-Coder-7B-Instruct
硬件:1 × A100 80GB
输入长度分布:P50 1K,P95 8K
输出长度分布:P50 256,P95 1K
SLO:P99 TTFT < 1s,P99 ITL < 80ms
目标:满足 SLO 下的最大 goodput

如果没有这些约束,QPS 数字几乎不可比较。短请求 benchmark 可以把 QPS 做得很好看,但一上线到长上下文 RAG,就会因为 prefill 和 KV cache 压力完全不同而失真。

工程上可以分三类压测:短问答压测看 API overhead 和 decode;长 prompt 压测看 prefill 和 prefix cache;长输出压测看 decode 吞吐和显存稳定性。

8. 可观测性 Observability

如果说前几节是在解释“LLM serving 如何工作”,那么可观测性(observability)关心的是:服务运行时到底发生了什么,为什么变慢,哪里快要违反 SLO,改动之后是否真的变好

在云原生系统里,可观测性通常围绕 metrics、logs、traces 展开;LLM serving 也一样,但会多出一组模型相关信号:输入/输出 token 长度、TTFT、ITL、KV cache、batch、prefix cache、GPU 显存、调度队列、goodput 等。它不是简单做一个 GPU 利用率面板,而是要把“用户请求”“模型阶段”“调度器状态”“硬件资源”连起来看。

8.1 观测对象与信号

一个 LLM 服务可以按层观测:

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

常见观测信号如下:

信号 记录什么 典型用途
Metrics 数值型时间序列 看趋势、报警、SLO、容量规划
Logs 离散事件和错误上下文 查失败请求、OOM、超时、异常栈
Traces 一次请求跨组件的阶段耗时 定位慢在排队、prefill、decode 还是网络
Profiles kernel、CPU、内存等细粒度性能剖析 优化热点、排查低 GPU 利用率
Benchmarks 受控负载下的实验结果 比较模型、框架、参数和硬件

一个简单类比:metrics 像体检指标,logs 像病历,traces 像从挂号到检查到开药的全流程时间线,benchmarks 则像标准化体能测试。

8.2 延迟指标

LLM serving 的延迟指标要按阶段拆:

指标 含义 主要对应阶段
TTFT Time To First Token,请求发出到第一个 token 返回 排队 + tokenization + prefill
ITL / TPOT Inter-Token Latency / Time Per Output Token,相邻输出 token 间隔 decode
E2E latency End-to-End latency,完整响应耗时 全流程
queue time 请求在调度器里等待的时间 调度
prefill latency 处理输入 prompt 的时间 prefill
decode latency 生成输出 token 的累计时间 decode

一个简化公式:

$$ T_{\mathrm{E2E}} \approx T_{\mathrm{TTFT}} + \bigl(N_{\mathrm{output}} - 1\bigr) T_{\mathrm{TPOT}} $$

如果输出很短,TTFT 主导体验;如果输出很长,TPOT 主导体验。流式聊天中,用户最先感知的是 TTFT,随后感知的是 token “蹦出来”的速度,也就是 ITL/TPOT。非流式接口则会把全部内容攒到最后返回,用户只感受到 E2E latency,但服务端仍然应该记录 TTFT 和 ITL,否则很难判断慢在 prefill 还是 decode。

8.3 吞吐与资源指标

吞吐有多个口径:

指标 单位 解释
QPS / RPS req/s 每秒完成多少请求
input throughput input tok/s 每秒处理多少输入 token
output throughput output tok/s 每秒生成多少输出 token
total token throughput tok/s 输入和输出 token 总量
per-GPU throughput tok/s/GPU 多卡场景下的资源效率

对 LLM 来说,单看 QPS 很危险。假设两个服务都是 10 QPS:服务 A 每个请求输出 20 token,服务 B 每个请求输出 1000 token,它们的 GPU 压力完全不同。更稳妥的表达是:“在某个输入/输出长度分布下,满足某个延迟 SLO 时,每张 GPU 每秒能产出多少 output tokens。”

还要同时看资源和调度指标:

  • GPU utilization、显存使用量、显存带宽、功耗、温度。
  • KV cache 使用量、block 使用率、prefix cache hit rate。
  • scheduler queue length、running requests、waiting requests。
  • preemption / eviction 次数。
  • HTTP 4xx/5xx、timeout、cancel、OOM。
  • cost per 1M tokens。

vLLM Production Metrics 就暴露了 generation tokens、prefix cache hits、preemptions、speculative decoding、KV connector 等指标;这些指标比单纯的 GPU utilization 更接近 LLM serving 的真实瓶颈。

8.4 分位数 SLO 与 Goodput

线上服务不要只看平均值。平均 TTFT 可能是 300ms,但 P99 TTFT 是 5s,用户投诉通常来自后者。

P50 TTFT = 300ms:一半请求首 token 延迟小于 300ms
P90 TTFT = 900ms:90% 请求小于 900ms
P99 TTFT = 5s:99% 请求小于 5s,最慢 1% 更糟

LLM 服务的长尾来自很多地方:个别超长 prompt、调度器排队、KV cache preemption、GPU kernel 抖动、网络慢客户端、模型冷加载、输出长度差异。博士生做实验时尤其要注意:只报告 mean latency 往往不够,至少应报告 P50/P90/P99 和长度分布。

Goodput 指的是满足 SLO 的有效吞吐。例如系统每秒完成 100 个请求,但只有 70 个请求满足 TTFT < 1sTPOT < 80ms,那么 goodput 是 70 req/s,而不是 100 req/s。

一个常见误区是追求最高 output tok/s。极限吞吐通常靠大 batch 换来,可能显著牺牲 TTFT 和 P99;真正上线时,要在 SLO 约束下比较吞吐。

8.5 日志与 Trace

Metrics 适合看趋势,但一次具体请求为什么慢,通常要靠日志和 trace。建议至少记录这些字段:

字段 例子 用途
request id req-123 串联 API server、scheduler、worker 日志
model Qwen2.5-Coder-7B-Instruct 多模型服务定位问题
input/output tokens 1024 / 256 解释延迟和成本
queue / prefill / decode time 20ms / 300ms / 4s 阶段归因
finish reason eos / length / stop / error 判断是否异常终止
error code timeout / oom / cancelled 失败分类

Trace 则把一次请求拆成多个 span,例如:

HTTP receive
  -> auth / routing
  -> apply chat template
  -> tokenize
  -> queue wait
  -> prefill
  -> decode step loop
  -> detokenize
  -> stream response

OpenTelemetry 是云原生里常用的开放标准,用来生成、收集和导出 traces、metrics、logs。对 LLM 应用层,还可以使用 LangfuseOpenLLMetry 这类工具观测 RAG、agent、tool call、prompt 版本、用户反馈和成本。它们和 serving runtime 的 metrics 不是替代关系:前者更偏应用链路,后者更偏模型服务和 GPU 资源。

日志里要谨慎处理 prompt 和输出内容。生产环境通常需要脱敏、采样、权限控制和保留周期策略,否则 observability 很容易变成隐私和合规风险。

8.6 监控技术栈

一个常见开源栈可以长这样:

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"]

关键组件:

组件 作用
Prometheus 拉取和存储时间序列 metrics,用 PromQL 查询和报警
Grafana 展示 dashboard,关联 metrics、logs、traces
OpenTelemetry 标准化 traces、metrics、logs 的采集和导出
NVIDIA DCGM Exporter 暴露 GPU 利用率、显存、功耗、温度等指标给 Prometheus
Loki / Tempo / Jaeger 存储日志和 trace,帮助做请求级问题定位

一个最小可用方案是:serving 框架暴露 /metrics 或 Prometheus 格式指标,Prometheus 定时 scrape,Grafana 展示 TTFT/ITL/QPS/GPU/KV cache 面板,Alertmanager 根据 P99、错误率、OOM、显存水位报警。等到业务复杂后,再补 OpenTelemetry tracing 和应用层 LLM observability。

8.7 压测与基准测试

压测是主动制造负载,监控是被动观察线上运行。两者要结合:压测告诉你系统极限和拐点,监控告诉你线上是否正在接近这些拐点。

为了让实验可复现、可比较,压测至少固定:

  • 模型名、权重精度、量化方式。
  • serving 框架版本和启动参数。
  • GPU 型号、数量、driver、CUDA。
  • 输入 token 长度分布。
  • 输出 token 长度分布或 max_tokens
  • stream / non-stream。
  • 并发模式:固定 concurrency、固定 request rate,还是 burst。
  • 采样参数:temperature、top_p、stop。
  • warmup 次数和统计窗口。

常见工具:

工具 适合做什么
vLLM benchmark vLLM 自带 benchmark,输出 request/input/output throughput、TTFT、TPOT、ITL 等
AIPerf 新一代 GenAI benchmarking 工具,支持多种 endpoint、数据集、并发和请求率控制
GenAI-Perf NVIDIA 旧工具,文档已说明逐步转向 AIPerf,但仍是理解 LLM benchmark 指标的好材料
Grafana k6 / Locust 通用 HTTP 负载测试,适合压 API gateway、鉴权、代理层,但需要自己统计 token 指标

读 benchmark 结果时,第一反应应该是看 workload,而不是直接比较榜单数字。短问答、长 prompt、长输出、多轮对话、RAG、agent 工具调用,会得到完全不同的 TTFT、ITL、吞吐和显存曲线。

9. 量化与显存优化

9.1 为什么需要量化

量化的目标是用更少 bit 表示原本高精度的数值。LLM 推理中最常见的是权重量化KV cache 量化

最简单的显存估算:

$$ M_{\mathrm{weights}} \approx N_{\mathrm{params}} \times \frac{q}{8} $$

其中 $q$ 是每个参数的 bit 数。因此,忽略 scale、zero point 等量化元数据时:

$$ M_{\text{7B, FP16/BF16}} \approx 7 \times 10^9 \times 2\text{ bytes} \approx 14\text{ GB} $$

$$ M_{\text{7B, INT4}} \approx 7 \times 10^9 \times 0.5\text{ bytes} \approx 3.5\text{ GB} $$

量化的收益有三类:

  1. 省显存:模型能放进更小的 GPU,或同一张 GPU 支持更大 batch。
  2. 提速度:decode 往往受显存带宽限制,数据更小,搬运更快。
  3. 降成本:同等负载可以用更少或更便宜的硬件。

代价是精度损失和 kernel 复杂度。不是所有量化都会变快:如果硬件或 runtime 没有高效 kernel,反量化开销可能抵消收益。

9.2 权重量化

权重量化作用于模型固定参数。常见方法:

方法 典型格式 关键思想
LLM.int8() INT8 混合精度 大部分用 INT8,离群维度保留高精度
GPTQ W4/W3 用近似二阶信息补偿量化误差
AWQ W4A16 根据激活分布保护重要权重通道
SmoothQuant W8A8 把激活离群值难度迁移到权重上
QLoRA NF4 + LoRA 冻结 4-bit 基座,只训练低秩适配器
GGUF K-quants Q4_K_M、Q5_K_M 等 llama.cpp/Ollama 本地推理常用格式

理解量化可以从一个标尺类比开始:原来用毫米级尺子测长度,现在只给 16 个刻度。刻度越少,记录越省空间,但误差越大。优秀的量化方法就是想办法把刻度放在“最常出现、最重要”的数值区域,而不是平均撒开。

在服务端 GPU 上,AWQ/GPTQ/FP8 等是否值得用,要看框架支持和 kernel 效率。vLLM 的 quantization 文档 列出了支持的量化后端;Ollama/llama.cpp 则更多围绕 GGUF 量化模型。

9.3 KV cache 优化

KV cache 不是模型文件里下载来的东西,而是推理时动态产生的。每个请求的每个历史 token,都会在每层 attention 中留下 Key 和 Value,用于后续 token 查询。

它解决的是“不要重复读题”的问题:生成第 100 个 token 时,模型不需要重新计算前 99 个 token 的全部中间状态,只需要复用 KV cache。但代价是显存随上下文长度和并发线性增长。

这里要和 1.4 节的上下文压缩区分开:上下文压缩是在模型计算前减少输入 token 或历史信息,KV cache 优化是在模型已经保留这些 token 后,降低缓存它们的显存和调度成本。前者改变 prompt,后者改变 runtime 管理方式。

KV cache 优化有几类:

  • dtype 降低:例如 FP16/BF16 改成 FP8 或 INT8。
  • KV cache quantization:如 KIVIKVQuant 等研究。
  • prefix cache:复用相同前缀的 KV。
  • paged/block 管理:减少显存碎片,例如 PagedAttention。
  • eviction/offloading:长上下文场景下把部分 KV 移出 GPU 或丢弃不重要 token。

KIVI 的一个重要观察是:Key 和 Value 的分布不同,Key 更适合 per-channel 量化,Value 更适合 per-token 量化。这说明 KV cache 量化不是简单把所有数压成 INT4,而要根据张量统计特性设计。

vLLM 已有 quantized KV cache 相关支持。实践中,短上下文、低并发场景开启 KV 量化未必更快;长上下文、高并发、显存紧张时,KV cache 优化才更容易体现价值。

9.4 量化选择要问什么

量化选型可以按下面的问题走:

  1. 目标是省权重显存,还是省 KV cache 显存?
  2. 场景是本地 CPU/消费级 GPU,还是服务端 A100/H100?
  3. 框架是否原生支持该量化格式?
  4. 是否有高效 kernel,还是只是“能跑”?
  5. 精度损失是否在业务可接受范围内?
  6. benchmark 是否覆盖真实输入输出长度和并发?

几个经验判断:

  • 本地运行优先看 GGUF 和 Ollama/llama.cpp 生态。
  • 单卡显存放不下模型时,先考虑权重量化。
  • 长上下文并发撑爆显存时,再重点考虑 KV cache 量化或 prefix cache。
  • H100/Blackwell 等新硬件上,FP8 往往更值得关注。
  • 量化后必须重新评估质量,特别是代码、数学、工具调用和结构化输出场景。

最终,量化不是单独的“压缩技巧”,而是 serving 系统设计的一部分。它改变权重大小、KV cache 容量、batch 上限、decode 带宽压力,最终反映到 TTFT、ITL、吞吐和 goodput 上。

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

2026-01-26 19:04:21

前言

近来我在摸索将Gemini Cli等终端agents融入工作流,目前总体感觉是——这类工具确实能够将脑力和精力从“杂活”和“需要但不感兴趣”的事情中解放出来,效率提升很多。这篇短文将介绍一个最近的实践案例。

作为博士生,我经常需要做的一件事是手动将电子邮件中的会议、报告、答辩等信息添加到Apple日历中。手动设置事件的标题、时间、地点、提前提醒时间、线上开会链接等内容属实麻烦。在ChatGPT出圈后,我一直想要开发一个功能——选中文字后直接在右键菜单中点击“添加日程”,程序就能够调用LLM去解析文字,生成Apple日历事件并导入到日历中。

然而,这个想法在脑海中,一直也没有去实现。最近,需要添加的事件尤其多,今晚吃饭回来后又想到了这一茬儿,于是打开Gemini Cli,给出需求,让它设计方案并实现,十分顺利。

现在我只需要在邮件中选中文字,点击右键,然后选择“添加日程”:

Xnip2026-01-26_19-56-57

稍等片刻,就能在日历中看到对应的事项了:

Xnip2026-01-26_19-58-17

代码仓库:https://github.com/brant-ruan/text2agenda

实践

为了提高成功率,我在Gemini Cli中将模型设定为gemini-3-pro-preview。初始需求描述如下:

我想要在当前目录下开发一个软件或者借助Mac上已有的automator等,它的功能是给我的macbook添加一个右键菜单“添加日程”,
使用场景是选中一段文字后,比如在Outlook中选中一个关于会议日程信息通知邮件的文字部分,右键,选择“添加日程”,
然后自动调用OpenAI/DeepSeek/Gemini等大模型的API,将其解析成.ics(苹果日程格式),然后自动导入到我的系统日历中。
这个项目允许用户通过配置文件配置API KEY等配置项。

很快,Gemini给出了实现方案:

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 & Configuration
* Structure: Create a directory for the project.
* Dependencies: Use Python'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 "next Friday" 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 "Right-Click" 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.

方案不错,批准了,请执行!很快,Gemini大哥就完成实现,并且给出了配置指南:

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 "Right-Click" Menu (Automator)
Since this part interacts with the macOS UI, you need to perform these steps manually once:

1. Open Automator (cmd+space, type "Automator").
2. Choose New Document -> 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 "$1"
6. Press Cmd+S to save. Name it Add Event (or "添加日程").

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) -> 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

按照它的提示配置完(文字解析使用的是物美价廉的gpt-5-nano),我测试了一下,检查日志,发现了问题,再次交互:

遇到了问题: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

Gemini很快完成了基于LiteLLM的实现。我再次提出需求:

现在就不必在配置文件中添加base_url了,不过我希望配置文件中支持设置提前几分钟提醒。

Gemini再次完成实现。现在,整个流程已经很流畅了。不过,我还希望更方便一些:

 我怎么才能将这个右键菜单从“Services”中移出来到一级菜单?

Gemini大哥不厌其烦地给出指点:

在 macOS 系统中,原生不支持用户直接将 Automator (自动操作) 创建的“快速操作”强行固定在右键的一级菜单中。macOS
会根据你启用的服务数量,自动决定是将它们折叠进“服务 (Services)”子菜单,还是显示在菜单底部。

要想实现“在一级菜单直接点击”,通常有以下两种方案:

方案一:清理“服务”菜单(原生,但不保证 100% 成功)
macOS 的逻辑是:如果启用的“服务”很少,它有时会将它们直接显示在右键菜单底部,而不折叠进子菜单。你可以尝试禁用不用的服务:
1. 打开 系统设置 (System Settings) -> 键盘 (Keyboard)。
2. 点击 键盘快捷键 (Keyboard Shortcuts) -> 服务 (Services)。
3. 在“文本 (Text)”分类下,取消勾选你不常用的所有服务,只保留刚才创建的“Add Event”。
4. 重启应用(如浏览器或邮件)看是否生效。
...

我再次尝试,发现了一个问题——设置的提前15分钟变成了15小时,于是再次发问:

现在似乎填写的提前15分钟最终在日历中变成了提前15小时,请改正。另外,我希望生成的事件能够体现出事件的本质特征。
例如,对于下面的文字,我希望生成的事件标题为Anonymous'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

Gemini再次更新代码中的prompt。至此,这个小玩意儿已经完全满足了我的使用需求。

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

2025-12-02 11:39:20

简介

本文在“安全研究GoSSIP”公众号上首发

老读者从首尔发回报道——11月17日到19日,第四十届软件工程领域国际会议ASE 2025在韩国首尔顺利举行。今年的ASE会议一共收到了1181篇投稿,最终录用了246篇论文。其中,来自中国的作者占比超过60%(含中国大陆及港澳台),体现了在软件工程领域的强劲实力。在所有提交和接收的论文中,研究主题的Top 3均为“AI和SE”、“测试和分析”、“安全及其他非功能特性”。

今天为大家推荐的论文是发表在这次ASE上的,一篇关于软件供应链漏洞影响评估的工作:Propagation-Based Vulnerability Impact Assessment for Software Supply Chains。该工作由新加坡国立大学梁振凯研究组完成并投稿。

随着软件生态变得日益复杂,软件供应链中的漏洞不仅影响漏洞所在的项目,还为依赖其所在项目的下游项目带来了安全风险——即使下游项目业务自身安全,攻击者还是能够通过利用这些上游漏洞来达到攻击下游项目的目的。因此,识别一个漏洞在软件供应链中的影响范围和规模变得越来越重要。

然而,现有工作并不能满足这一需求。一方面,已有研究要么不精确——只考虑了软件包层级(package-level)的依赖关系,没有进一步考虑漏洞所在函数是否真正可达,从而引入了大量虚假告警,要么是不完整的——通过在软件生态中采样等方式考察了局部或部分的函数调用图级(call-graph-level)依赖关系;另一方面,当前已被广泛采用的漏洞评估指标,如CVSS、CWE等,都只能用于对漏洞本身进行评估,而无法反映漏洞在软件供应链中的影响。

为了实现精确、完整的软件供应链漏洞影响评估,受程序分析中的数据流分析算法启发,论文首先提出了一个基于worklist并支持多层级剪枝的漏洞传播分析算法,能够精确、高效地在函数调用图层级、面向漏洞所在整个软件供应链生态实现漏洞传播分析。在此基础上,论文进一步提出了漏洞传播评分系统(Vulnerability Propagation Scoring System,简称VPSS),对漏洞传播分析结果进行建模,帮助人们对漏洞在软件生态中的影响进行量化和感知,支撑更加科学、及时的漏洞治理工作。

论文作者在Java Maven生态系统上实现了工具原型,并使用100个Maven生态中的漏洞进行了测试。评估结果显示,论文提出的算法能够完成全Maven生态的漏洞影响评估,相应的VPSS分数能够提供有价值的漏洞影响规模情报。

该论文的贡献主要包括:

  • 设计了一种新的漏洞传播分析算法,实现在函数调用图层级、面向漏洞所在整个软件供应链生态识别受漏洞影响的下游依赖项目。
  • 提出了漏洞传播评分系统(Vulnerability Propagation Scoring System,简称VPSS),这是第一个专门用于量化软件供应链中漏洞影响的动态评估指标。
  • 在Java Maven生态系统上实现了算法和指标的工具原型,并使用100个Java Maven生态中的漏洞进行了测试。工具原型和用于测试的漏洞数据集已经开源

背景

软件供应链安全已成为信息安全领域的焦点。一个上游库的漏洞(例如Heartbleed 或 Log4Shell )就可能使其下游成千上万的软件项目处于风险之中 。因此,在漏洞被披露后,快速准确地评估其“影响范围”(Impact Scope)至关重要 。然而,现有的研究在尝试解决这个问题时,面临两大局限:

缺乏准确且完整的传播分析:

  1. 粒度太粗:大多数分析停留在“包级别”(package-level)。它们只检查下游项目是否“声明”或包含了对脆弱包的依赖。但这会带来大量的假阳性(false positives),因为下游项目很可能只是依赖了那个包,却根本没有调用到漏洞所在的脆弱函数(vulnerable function,VF)。
  2. 范围太窄:另一部分研究虽然尝试了更精确的“调用图”(call graph, CG)级别分析,但它们的能力非常受限——要么无法处理复杂的“全生态系统”依赖 ,要么只分析有限的下游项目 ,并且常常忽略了“传递性依赖”带来的深层风险。

作者进一步从六个方面衡量、对比了现有工作,并得出理想的漏洞传播分析应该具备的特性:

related

缺乏量化供应链“传播影响”的指标:例如,我们熟知的CVSS评分系统,是用来评估漏洞本身的固有特征(如攻击复杂度、所需权限等),但它无法衡量漏洞在供应链中的“传播影响”。一个CVSS 9.8分的漏洞,如果只存在于一个无人问津的库中,其真实的供应链风险可能远小于一个7.5分但存在于核心基础库中的漏洞。CVSS v4.0的官方FAQ也明确承认了CVSS不适用于衡量供应链的传播影响。

针对上述痛点,作者希望设计一个既准确(CG级别)又完整(全生态、传递性)的分析框架,并提出一个专门用于量化传播影响的新指标。

研究方法

为了实现这一目标,作者设计了一个包含四个步骤的语言无关的自动化漏洞影响评估框架,如下图所示:

overview

一、依赖图构建 (Dependency Graph Construction)

首先,作者需要构建一个描述目标软件生态中全部依赖关系的依赖图。但如果直接构建“项目-版本”(PV)级别的图,规模将大到无法处理(例如Java Maven生态有超过1500万个PV )。作者的关键洞察是:转而构建P-level(项目级别)的依赖图。这个图的规模要小得多(Maven中约66万个P ),可以作为高效的预过滤器 ,同时将具体的版本依赖信息(deps.json)用于后续的精确查询 。这个依赖图只需要构建一次,然后随着软件生态官方仓库做动态增量更新即可。

二、脆弱函数识别 (Vulnerable Function Identification)

为了进行CG级别的分析,我们必须首先知道“漏洞在哪几个函数里”。对此,作者采用了一种LLM辅助的优化策略 :首先,使用传统的patch-based方法从补丁中提取候选的VF。然后,利用LLM(作者测试了GPT-4o-mini等模型)强大的代码语义理解能力,自动过滤掉那些与漏洞无关的修改,包括“语义等价的修改”(如重命名变量、增删空格或注释等)和“语义改变但无关的修改”(如增删日志(logging)代码等)。

vfi

三、漏洞传播分析 (Vulnerability Propagation Analysis)

这是作者提出的分析框架的核心。他们设计了一种新颖的“分层基于工作列表(Hierarchical Worklist-based)”的算法:

propagation

为什么用Worklist?论文的算法借鉴了数据流分析中的经典“工作列表”思想 。这使作者能系统地遍历整个生态依赖图,有效处理复杂的共享依赖,甚至是真实世界中存在的“循环依赖”问题 ,确保分析的健全性(soundness)。

为什么是Hierarchical(分层)?这是为了效率。直接对全生态构建CG进行分析是计算上不可行的 。作者设计了三级“分层剪枝(Hierarchical Pruning)”策略 ,逐层过滤掉海量的“假阳性”依赖关系:

  • 版本剪枝(Version-based Pruning):检查下游项目是否依赖了受影响的上游版本 。
  • 导入剪枝(Import-based Pruning):检查下游项目是否 真正导入(import)了上游包的内容 。在这一步,作者还通过识别“内在范围”(intrinsic scope),巧妙地处理了“fat JAR”等打包问题。
  • CG级别剪枝(CG-level Pruning): 最后,作者才对剩下的候选PV构建CG,检查下游项目是否真正调用了上游的脆弱入口点(entry-points)。

四、VPSS计算 (VPSS Calculation)

在精确分析完传播范围后,作者引入了一个全新的量化指标:VPSS(Vulnerability Propagation Scoring System),用于量化漏洞在供应链中的影响范围和演变。它和CVSS一样,采用0-10分制,并分为低、中、高、危急四个等级,易于理解和使用。注意,VPSS是“时间感知(Time-aware)”的,因此可以计算漏洞在任意时间点(例如t)的VPSS分数(计算时会排除t之后发布的P和PV),从而动态追踪其影响力的演变。

VPSS的核心公式是VPSS_raw = PBF x PDF,其中:

  • PBF(传播广度因子): 衡量漏洞传播的宽度。它综合考虑了受影响的下游P和PV的数量(包括直接和传递依赖)。
  • PDF(传播深度因子): 衡量漏洞传播的深度。它基于传播链的最大和平均长度来计算 。

详细的计算过程如下:

vpss

评估

作者在Java Maven软件生态上实现了分析框架的原型 ,并抽取了100个真实CVE进行评估 。

一、漏洞传播分析的有效性和可扩展性如何?

发现1:分层剪枝策略效果显著。在分析100个CVE的过程中,平均97.8%的项目(P)和99.2%的项目版本(PV)被成功剪枝。

发现2:假阳性比例大。如下图所示,如果仅基于依赖声明(即“initial”状态),平均一个漏洞会涉及上千个项目和数万个PV。而作者的分析(v3)最终确认,高达94.9%的PV是假阳性(即它们虽然依赖了,但根本没调用)。这大大降低了CG的构建成本 。

eval1

二、VPSS分数能告诉我们什么?

作者计算了这100个CVE在披露后24个月内(每30天一个快照,t0到t23)的VPSS分数演变。

发现1:VPSS能动态反映生态修复趋势。如下图所示,绝大多数漏洞的VPSS分数都随时间推移呈下降趋势。这完全符合预期:随着补丁被应用、下游项目逐渐迁移到安全版本,漏洞的实际传播影响在不断减小。

发现2:VPSS能捕获异常的“延迟更新”现象。作者也发现了一个有趣的特例:CVE-2016-3086的分数在t1时刻反而异常上升了(从6.95升至7.19)。作者推测这可能是由于下游项目“延迟更新”其依赖所导致的。

eval2

发现3:大部分漏洞的传播影响有限。如下面的箱线图所示,在100个CVE中,绝大多数的VPSS分数都保持在较低水平,并在两年内趋于稳定。

eval3

三、案例分析:CVE-2016-5393

CVE-2016-5393在测试数据集中VPSS得分最高(t0时为7.35)。作者分析发现,它在披露时直接影响了228个项目,并传递影响了另外154个项目。其最长传播链达到了7跳,并且在24个月后分数依然高达6.86,反映了生态中“长尾依赖”的顽固性。

eval4

论文链接:https://arxiv.org/pdf/2506.01342

VPSS开源项目链接:https://github.com/brant-ruan/vpss

作者团队十分欢迎大家交流合作,一起推动软件供应链漏洞治理工作!

作者信息

投稿作者简介:阮博男,2024级新加坡国立大学博士生,导师为梁振凯老师,目前主要研究方向为漏洞挖掘和漏洞评估,相关研究成果发表在ASE、USENIX Security、RAID等软件工程和信息安全领域的国际会议上,曾获得RAID 2024最佳实践论文奖和USENIX Security 2025杰出论文奖,也曾在BlackHat Asia、KCon、CIS等国内外知名工业界信息安全会议上发表演讲。

个人主页:https://profile.wohin.me

如果对NUS Curiosity Research Group感兴趣,梁振凯老师也在招收系统安全方向的博士生和博士后,详细信息可以参考:

梁振凯老师的个人主页:https://www.comp.nus.edu.sg/~liangzk

NUS Curiosity研究小组主页:https://nus-curiosity.github.io

很多年

2025-09-13 08:00:00

作者:北岛


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

致谢

2025-07-21 08:00:00

作者:黄国平


我走了很远的路,吃了很多的苦,才将这份博士学位论文送到你的面前。二十二载求学路,一路风雨泥泞,许多不容易。如梦一场,仿佛昨天家人才团聚过。

出生在一个小山坳里,母亲在我十二岁时离家。父亲在家的日子不多,即便在我病得不能自己去医院的时候,也仅是留下勉强够治病的钱后又走了。我十七岁时,他因交通事故离世后,我哭得稀里糊涂,因为再得重病时没有谁来管我了。同年,和我住在一起的婆婆病故,真的无能为力。她照顾我十七年,下葬时却仅是一副薄薄的棺材。另一个家庭成员是老狗小花,为父亲和婆婆守过坟,后因我进城上高中而命不知何时何处所终。如兄长般的计算机启蒙老师◼️◼️没能看到我的大学录取通知书,对我照顾有加的师母也在不惑之前匆匆离开人世。每次回去看他们,这一座座坟茔都提示着生命的每一分钟都弥足珍贵。

人情冷暖,生离死别,固然让人痛苦与无奈,而贫穷则可能让人失去希望。家徒四壁,在煤油灯下写作业或者读书都是晚上最开心的力。如果下雨,保留节目就是用竹笋壳塞瓦缝防漏雨。高中之前的主要经济来源是夜里抓黄鳝、周末钓鱼、养小猪崽和出租水牛。那些年里,方圆十公里的水田和小河都被我用脚测量过无数次。被狗和蛇追,半夜落水,因蓄电瓶进水而摸黑逃回家中:学费没交,黄鳍却被父亲偷卖了,然后买了肉和酒,都是难以避免的事。

人后的苦尚旦还能克服,人前的尊严却无比脆弱。上课的时候,因拖欠学费而经常被老师叫出教室约谈。雨天湿漉着上课,屁股后面说不定还是泥。夏天光着脚走在滚烫的路上。冬天穿着破旧衣服打着寒颤穿过那条长长的过道领作业本。这些都可能成为压垮骆驼的最后一根稻草。如果不是考试后常能从主席台领奖金,顺便能贴一墙奖状满足最后的虚荣心,我可能早已放弃。

身处命运的旋涡,耗尽心力去争取那些可能本就是稀松平常的东西,每次转折都显得那么的身不由己。幸运的是,命运到底还有一丝怜惜。进入高中后,学校免了全部学杂费,■■■一家帮助解决了生活费。进入大学后,计算机终于成了我一生的事业与希望,胃溃疡和胃出血也终与我作别。

我很庆幸保研时选择了自动化所,感谢研究生部的老师们将我从别的部门调剂到模式识别实验室,感谢导师宗成庆老师选择了我,宗老师将我引入了科学研究的大门,博士这五年无疑是我过去最幸福的时光。惭愧的是,离宗老师的期望显然还有很远的距离,我也知道本可以做得更好。这一段经历已经成为我这一生值得回味的美好瞬间之一。我很喜欢人机交互式机器翻译这个题目,但也仅开了个头。在未来,希望能有机会弥补这段遗憾。

从家出发坐大巴需要两个半小时才能到县城,一直盼着走出大山。从炬光乡小学、大寅镇中学、仪陇县中学、绵阳市南山中学,到重庆的西南大学,再到中科院自动化所,我也记不清有多少次因为现实的压力而觉得自己快扛不下去了。这一路,信念很简单。把书念下去,然后走出去,不枉活一世。世事难科,未来注定还会面对更为复杂的局面。但因为有了这些点点滴滴,我已经有勇气和耐心面对任何困难和挑战。理想不伟大,只愿年过半百,归来仍是少年,希望还有机会重新认识这个世界,不辜负这一生吃过的苦。最后如果还能做出点让别人生活更美好的事,那这辈子就赚了。

最近这些年,特别感谢■■■■■■■■■■■■■■■■重塑了我的人生。

感谢■■■老师和■■■老师为我的研究提出诸多建设性的意见和建议。与他们的交流过程中,我也被他们坦诚为人、踏实做事的处事风格所感染。感谢所有和我一起在课题组工作的老师们和学习的同学们:■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■、■■■。我们在研究上共同努力,互相学习,如亲兄弟姐妹一般谈笑风生。不认大学将来在休息,我想我们都不会忘记这些年一起度过的时光。感谢课题组■■■女士,感谢模式识别综合办实验室的■■■、■■■和■■■等老师,感谢研究生部的■■■、■■■、■■■和■■■等老师,他们为我的学习和工作给予了极大的帮助。这会成为我日后工作的动力,希望最后能成为课题组值得骄傲的学生。

感谢父母给了我生命,感谢婆婆和叔辈对我的抚养,感谢■■■■■■■■■■■■■■■■■■对我的牵挂,感谢■■■和■■■对我的支持,感谢一路上让我生活变得美好或者不那么美好的人和事。

最后,对参加论文评审、答辩的各位老师表示衷心的感谢!