在做llama-server压测中遇到高并发与大上下文不能推理直接崩溃问题:
.\llama-server.exe -m C:\......models\Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf --host 0.0.0.0 --port 8080 -ctk q4_0 -ctv q4_0 -ngl 999 -a Qwen3.6-35B -np 32 -c 4500000
0.52.356.143 I slot get_availabl: id 31 | task -1 | selected slot by LRU, t_last = -1
0.52.356.191 I slot launch_slot_: id 31 | task 0 | processing task, is_child = 0
0.56.729.005 I slot print_timing: id 31 | task 0 | n_gen = 179, tg = 59.11 t/s, tg_3s = 59.44 t/s
0.56.866.037 I slot print_timing: id 31 | task 0 | prompt eval time = 1361.68 ms / 11 tokens ( 123.79 ms per token, 8.08 tokens per second)
0.56.866.051 I slot print_timing: id 31 | task 0 | eval time = 3148.22 ms / 187 tokens ( 16.93 ms per token, 59.08 tokens per second)
0.56.866.052 I slot print_timing: id 31 | task 0 | total time = 4509.90 ms / 198 tokens
0.56.866.053 I slot print_timing: id 31 | task 0 | graphs reused = 186
0.56.866.079 I slot release: id 31 | task 0 | stop processing: n_tokens = 197, truncated = 0
0.57.834.622 I slot get_availabl: id 30 | task -1 | selected slot by LRU, t_last = -1
0.57.834.674 I slot launch_slot_: id 30 | task 189 | processing task, is_child = 0
D:/a/llama.cpp/llama.cpp/ggml/src/ggml-backend.cpp:359: GGML_ASSERT(tensor->data != NULL && "tensor not allocated") failed
压测指令 evalscope perf --api openai --model qwen3.6-35b-q4 --url http://127.0.0.1:8080/v1 --tokenizer-path /....../Qwen/Qwen3.6-35B-A3B --dataset random --min-prompt-length 512 --max-prompt-length 1024 --min-tokens 1024 --max-tokens 2048 --prefix-length 0 --parallel 1 2 4 8 16 32 --number 4 8 16 32 64 128 --warmup-num 8 --stream --duration 300 --visualizer wandb --name single-short-in-long-out --wandb-api-key wandb_v1_Qn6P6W29imIJ836NidR
原因:llama-server 自身在多 slot 场景下做 KV-cache / prompt-cache 状态序列化时,读到了一个从未被分配的 tensor,触发 GGML_ASSERT直接 abort 进程。
日志看时序很清晰:
- slot 31 正常完成了 task 0 的生成(59 t/s,198 tokens),说明模型加载、warmup、首次推理都没问题;
- 随后 LRU 选中 slot 30、launch task 189,还没来得及输出任何 timing 就立刻崩了;
- 崩溃点
ggml-backend.cpp:359: GGML_ASSERT(tensor->data != NULL && "tensor not allocated"),这行代码位于ggml_backend_tensor_get/put,是在读写某个 KV-cache tensor 的数据时发现该 tensor 根本没被分配过。
关联issue:https://github.com/ggml-org/llama.cpp/issues/26128 。
llama-server 的 prompt cache 在做 slot 状态保存时走 server_slot::prompt_save()→ llama_context::state_seq_get_data()→ ggml_backend_tensor_get()这条序列化路径,而这条路径假设 KV tensor 在主机内存里有直接的 data 指针 。当 KV tensor 落在 RPC 后端(无本地指针)这类"非平凡"后端 buffer 上时,assert 在 buf->iface.get_tensor()(本可以正确处理远端 tensor 的接口)被调用之前就先炸了。
触发条件与你的日志完全对得上 :issue 明确说 -np = 1时潜伏不炸(没有 idle slot 可保存、对话永远命中自己的 slot),-np ≥ 2 时只要有新任务在其它 slot 空闲时进来就立刻 abort。它还列出了两个调用点:LRU 换占 slot 时(get_available_slot())和每个新任务对所有 idle slot 做缓存保存时(process_single_task())。本地日志里 slot 30 被 LRU 选中、launch task 189 即崩,就是这个模式的翻版。
处理方法:
--cache-ram 0完全禁用 prompt cache,崩溃消失,作者在并发负载下验证稳定;我验证也是可用的。- 另外一种方式是把--np 改16 也是可以正常运行的。
- 两个重要澄清:
--no-cache-prompt无效 (server prompt cache 是独立机制);--no-cache-idle-slots只能去掉一个调用点,LRU 路径仍在,不够。 - 关键安慰:slot 内的前缀复用不受
--cache-ram 0影响(实测后续轮次cache_n=53仍命中),server prompt cache 只在"会话数 > slot 数"时才有收益,对你 32 slot、每 slot 独立请求的压测场景本来就是零收益。
引出 --cache-ram 参数 ,它控制的是 server 的"跨 slot prompt cache"(RAM 侧 KV 状态存档)预算,0 表示完全关闭这套机制;它是独立于 slot 内前缀复用和 --no-cache-prompt的第三个缓存层。
引出了三层缓存机制:
|---------------------------|--------------------------------|------------|-----------|-----------------------------|
| 层级 | 参数 / 开关 | 作用范围 | 存储位置 | 触发场景 |
| Slot 内前缀复用 | 无(自动启用) | 同一 slot 内 | GPU 显存 | 同一对话多轮请求 |
| 跨 Slot RAM 状态存档(cache 主体) | --cache‑ram <MB>(默认 8192MiB) | 所有 slot 之间 | 主机内存(RAM) | 会话数 > slot 数,LRU 换占 slot 时 |
| GPU 侧上下文检查点 | --ctx‑checkpoints <N>(默认 32) | 单个 slot 内 | GPU 显存 | 生成过程中按 token 间隔定期保存 |
--cache-ram 的8192 MiB预算同时服务第二、三层两个机制, 设置 --cache-ram 0会同时禁用检查点创建和跨slot缓存。
|--------|---------------------|-----------------------|------------------------------------|
| 特性 | 第一层: Slot 内前缀复用 | 第二层:跨 Slot RAM 存档 | 第三层:上下文检查点 |
| 存储位置 | GPU 显存 | 系统内存 | 系统内存 |
| 控制参数 | 自动启用 | --cache‑ram | --ctx‑checkpoints, --cache‑ram |
| 主要场景 | 多轮对话 | 会话数 > slot 数 | 长上下文 / 上下文移位 |
| 存储内容 | 完整 KV 前缀 | 完整 KV 状态 | 部分不可重建状态 |
需要详细解释的第三层是llama.cpp的上下文检查点系统,用于处理长上下文和上下文移场景。它通过定期保存KV状态的"部分快照"来避免完全重新处理长提示。
第三层检查点参数
|----------------------------------|-----------------|----------|
| 参数 | 作用 | 默认值 |
| --ctx-checkpoints / -ctxcp | 每个 slot 的最大检查点数 | 32 |
| --checkpoint-min-step / -cms | 创建检查点的最小令牌间隔 | 8192 |
| --cache-ram / -cram | 检查点使用的 RAM 预算 | 8192 MiB |
实际应用场景
- 长上下文处理:当上下文超过模型限制时,通过检查点移,避免完全重新处理
- 代理工具调用:在本地编码代理等场景中,工具调用后恢复状态,大幅减少冗余计算
- 混合注意力模型:对Qwen3.5等具有滑动窗口注意力的模型特别有用
第三层是缓存"不可重建的中间状态"
在llama.cpp的上下文检查点系统中,"不可重建的中间状态" 是指那些无法通过简单截断KV cache来恢复的模型内部状态。
|----------|---------------------------------|------------------------------------------|
| 状态类型 | 完整注意力 KV Cache | 不可重建的中间状态 |
| 存储形式 | 每个 token 都有独立的 Key/Value 对 | 滑动窗口注意力 (SWA) 的 KV 或 递归层 (如 Mamba) 的运行状态 |
| 能否截断恢复 | 可以,llama_memory_seq_rm () 能任意删除 | 不能,一旦滑动或覆盖,旧数据就丢失 |
| 检查点是否存储 | 不存储(仍在 slot 中,可截断恢复) | 必须存储(这是检查点唯一保存的内容) |
两种典型不可重建状态
A. 滑动窗口注意力(SWA)的KV Cache
对于采用SWA的模型(如Qwen3.6、Mistral),每个注意力层只维护一个固定大小的"窗口",窗口外的token的KV会被丢弃。
SWA(滑动窗口注意力)机制:
- 不是"压缩+新内容保留" ,而是维护固定大小的窗口
- 当处理新Token时,窗口外的最旧Token的KV会被永久丢弃
- 窗口内的Token可能包含新Token和部分旧Token
B. 递归层的运行状态(如Mamba、RWKV)
对于Mamba、RWKV等状态空间模型 ,它们不存储每个token的KV ,而是维护一个单一的"运行状态" ,这个状态在每个token处理时都会被原地更新。
递归模型(如Mamba)机制:
- 没有独立的KV对 ,而是维护一个单一的运行状态
- 这个状态在每个Token处理时都会被原地更新(覆盖)
检查点设计的主要目标。当处理一个长prompt后,后续请求如果与之前的对话有共同前缀,server会尝试找到匹配的检查点,恢复到某个中间状态,只计算新增的token。
典型场景:多轮对话中,用户发送新请求(包含之前所有对话历史+新问题)。如果没有检查点,server需要重新处理整个对话历史;有了检查点,可以恢复到之前某个状态,只处理新问题部分。