fix: 12 bug fixes from comprehensive review — 51 tok/s verified on RTX 5090

P0 fixes (blocking usability):
- FIX-01: thread-local cuBLAS handle (was creating/destroying per matmul)
- FIX-16: EOS token no longer leaks into API responses
- FIX-17: max_seq_len configurable via --max-seq-len (default 2048, was hardcoded 256)
- FIX-18: max_tokens clamped to available seq space, prompt overflow returns 400

P1 fixes (bugs & performance):
- FIX-07: CachingAllocator wired into all hot paths (to_device, embedding, rope, concat)
- FIX-08: CudaDeviceProp buffer increased to 32KB for CUDA 12.9 safety
- FIX-09: tokenizer byte_fallback graceful degradation (was panic)
- FIX-19: causal mask uses -INFINITY instead of -1e9 (BF16 supports inf)
- FIX-20: LayerNorm rewritten to numerically stable two-pass algorithm
- FIX-21: min block size guard (32 threads) for LayerNorm/RMSNorm launches

P2 fixes (improvements):
- FIX-22: Option<GpuKVCache> + take() eliminates dummy KV cache allocations
- FIX-23: RoPE cache no longer artificially capped at 8192 positions

Verified on dash5 (RTX 5090): 51 tok/s batch=1, 74 tok/s 2-concurrent, 1.7-3.3x HF transformers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 14:13:43 +08:00
parent a67e724119
commit 986a289616
14 changed files with 285 additions and 292 deletions

View File

@@ -1,287 +1,214 @@
# xserv — To Be Fixed
# xserv — To Be Fixed (2026-05-23 审查更新)
> 最严格审查产出的修复清单。每项修复有明确验收标准,禁止 reward hacking
> 全面审查产出的修复清单。每项修复有明确验收标准。
> 优先级: P0 (阻塞可用性) > P1 (严重bug/性能) > P2 (重要改进) > P3 (设计债务)
---
## FIX-01: 全局 cuBLAS handle消除 per-call 创建 [P0-性能]
## 第一批P0 — 阻塞可用性
**问题**: `gemm.rs` 中每次 `matmul` / `batched_matmul` 调用都 `cublasCreate_v2` + `cublasDestroy_v2`。Qwen3-8B 一次 forward 约 168 次 matmul每次创建/销毁 handle 耗费数毫秒。
### FIX-01: 全局 cuBLAS handle [P0-性能] ❌未修
**问题**: `gemm.rs``matmul` (line 146) 和 `batched_matmul` (line 224) 每次调用都 `CublasContext::new()` 创建+销毁 handle。Qwen3-8B 一次 forward ~252 次 matmul。
**修复要求**:
- 使用 thread-local 或全局单例 cuBLAS handle
- handle 生命周期覆盖整个进程,不在 matmul 内创建/销毁
- `CublasContext` 支持 `set_stream` 切换 stream
- 使用 thread-local 单例 cuBLAS handle
- handle 生命周期覆盖整个进程
- `matmul` / `batched_matmul` 函数体内不再有 `CublasContext::new()`
**验收标准**:
1. `grep -rn "cublasCreate_v2" crates/xserv-kernels/src/gemm.rs` 只出现 1 次(初始化处)
2. `matmul``batched_matmul` 函数体内不再有 `CublasContext::new()`
3. 编译通过,现有 gemm_test 全部通过
1. `grep -n "CublasContext::new" crates/xserv-kernels/src/gemm.rs` 只出现 1 次(thread_local 初始化处)
2. 编译通过,现有 gemm_test 全部通过
---
## FIX-02: 移除不必要的 cudaDeviceSynchronize [P0-能]
### FIX-16: EOS token 泄漏到 API 响应 [P0-能] ❌新发现
**问题**: 几乎每个 kernel wrapper 结尾都有 `xserv_cuda::device::synchronize()`(即 `cudaDeviceSynchronize`),完全杀死 GPU pipeline
**问题**: `engine.rs:218``emit_token` 先发 `GenerateEvent::Token { text: "<|im_end|>" }` 再发 `Done``api.rs:110-111` 把所有 Token text 拼到 content 里,导致最终响应包含 `<|im_end|>` 乱码
**修复要求**:
- 删除所有 kernel wrapper 中的 `device::synchronize()` 调用
- 仅在需要读回 GPU 数据到 CPU 时同步(如 `sample_greedy`, `to_device(Cpu)`, benchmark
-`Tensor::to_device(Cpu)` 路径中已有隐式同步(`cudaMemcpy` 是同步的),不需要额外 sync
- 如果 kernel 使用 null stream默认 stream`cudaMemcpy` 会隐式等待默认 stream 上的所有操作
- `emit_token` 中,当 token 是 EOS 时,不发送 Token event或发送空 text直接发 Done
- 或者: API 层收到 Done 时丢弃最后一个 token 的 text如果 finish_reason == "stop"
**验收标准**:
1. `grep -rn "device::synchronize" crates/xserv-kernels/src/` 返回 0 行
2. `grep -rn "device::synchronize" crates/xserv-model/src/` 只出现在 benchmark binary 中,不在 forward path 中
3. 编译通过,现有测试全部通过
4. 模型推理结果与修复前 bit-exact 一致greedy decode 相同 prompt 产生相同 token 序列)
---
## FIX-03: 修复 Chat Template [P0-功能]
**问题**: `api.rs``build_prompt` 只是简单拼接文本,没有 ChatML special tokens。Qwen3 模型收到的 prompt 没有对话结构。
**修复要求**:
- 生成符合 Qwen3 ChatML 格式的 prompt
```
<|im_start|>system\n{content}<|im_end|>\n<|im_start|>user\n{content}<|im_end|>\n<|im_start|>assistant\n
```
- 如果没有 system message跳过 system 部分
- 如果有多轮 assistant/user 交替,按顺序生成
- 结尾始终是 `<|im_start|>assistant\n`(让模型生成 assistant 回复)
**验收标准**:
1. 单元测试: 给定 `[{role: "user", content: "Hello"}]`,生成的 prompt 字符串包含 `<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n`
2. 单元测试: 给定 system + user + assistant + user 四条消息,格式正确
1. 发送请求,响应 content 不包含 `<|im_end|>` 或其他 special token 文本
2. streaming 模式下最后一个 content chunk 不是 EOS 文本
3. 编译通过
---
## FIX-04: 修复 `is_finished` 硬编码 EOS [P0-功能]
### FIX-17: max_seq_len 硬编码 256 [P0-功能] ❌新发现
**问题**: `engine.rs:160` 硬编码 `last == 151645` 作为 EOS 判断
**问题**: `engine.rs:53` 硬编码 `let max_seq_len = 256`,超过就 KV cache panic
**修复要求**:
- `Sequence` struct 增加 `eos_token_id: Option<u32>` 字段
- 在 `make_sequence` 中从 tokenizer 获取 EOS token ID
- `is_finished` 使用该字段判断
- `Engine::load` 接受 `max_seq_len` 参数(或从 config 读取,上限为 config.max_seq_len()
- `main.rs` 中通过命令行参数或环境变量传入,默认值改为 2048
- 同步更新 RoPE cache 上限(当前 `qwen3.rs:45` 限制 8192应与 max_seq_len 一致)
**验收标准**:
1. `grep -rn "151645" crates/xserv-server/` 返回 0 行
2. `is_finished` 函数不包含任何硬编码 token ID
1. `grep -n "let max_seq_len = 256" crates/xserv-server/` 返回 0 行
2. 启动 server 时 `--max-seq-len 4096` 可用
3. 编译通过
---
## FIX-05: 修复 `Storage::device()` 丢失设备信息 [P1-Bug]
### FIX-18: max_tokens 无上限校验 [P0-功能] ❌新发现
**问题**: `storage.rs:43` 对所有 GPU storage 返回 `Device::Cuda(0)`,不追踪实际设备
**问题**: API 不校验 `max_tokens`,客户端可发 `max_tokens: 1000000` 导致 KV cache panic
**修复要求**:
- `StorageInner::Cuda` 增加 `device: u32` 字段
- `Storage::cuda()` 接受 device 参数,或从 `GpuBuffer` 推断
- `Storage::device()` 返回实际设备
- 所有创建 `Storage::cuda()` 的调用点更新
- `api.rs` 中 clamp `max_tokens``engine.max_seq_len - prompt_tokens.len()`
- 如果 prompt 已超过 max_seq_len返回 400 错误
**验收标准**:
1. 创建一个 `Device::Cuda(3)` 的 tensor`tensor.device()` 返回 `Device::Cuda(3)`
1. 发送 `max_tokens: 999999`,不 panic正常生成到 seq_len 上限
2. 发送超长 prompt> max_seq_len返回 HTTP 400
3. 编译通过
---
## 第二批P1 — 严重 bug/性能
### FIX-07: 使用 CachingAllocator [P1-性能] ❌未修
**问题**: `CachingAllocator` 已实现(`allocator.rs`)但从未使用。所有 GPU 分配直接 `cudaMalloc`
**修复要求**:
- `Tensor::empty` 对 GPU device 使用 `cached_alloc` 而非 `GpuBuffer::alloc`
- `GpuBuffer::Drop` 调用 `cached_dealloc` 归还到池(而非 `cudaFree`
- 或者更简单:在 `GpuBuffer::alloc` 内部接入 caching allocator全局透明替换
**验收标准**:
1. 连续运行 10 次 decode step`cudaMalloc` 调用次数应显著低于总分配次数
2. 编译通过,现有测试通过
3. 推理结果与修复前一致
---
### FIX-08: CudaDeviceProp FFI 安全性 [P1-Bug] ❌未修
**问题**: `ffi.rs:31``_pad: [u8; 4096]` 猜测 `cudaDeviceProp` struct 大小CUDA 12.9 可能更大。
**修复要求**:
- 增大 pad 到 `[u8; 8192]` 或使用 `cudaDeviceGetAttribute` 替代 name 查询
- 可参考 `device.rs` 中已有的 `cudaDeviceGetAttribute` 用法
**验收标准**:
1. `device_info()` 返回正确的 device name
2. 编译通过
---
### FIX-09: Tokenizer byte_fallback panic [P1-Bug] ❌未修
**问题**: `bpe.rs:176-182` 中 Qwen3 tokenizer 遇到不在 vocab 的单字节时 panic。
**修复要求**:
-`byte_fallback == true` 且单字节不在 vocab 时,查找 `<0xNN>` 格式 token
- 如果 `<0xNN>` 也不存在,返回 unk_token_id而非 panic
**验收标准**:
1. 包含所有 256 个字节值的字符串可以 encode 不 panic
2. 编译通过
---
### FIX-19: 因果掩码 -1e9 应改为 -inf [P1-Bug] ❌新发现
**问题**: `csrc/attention/causal_mask.cu:31``-1e9f` 代替 `-inf`,注释说 "BF16 没有 -inf" 但这是错误的。
**修复要求**:
- BF16 路径改为 `__float2bfloat16(-INFINITY)`
- F32 路径改为 `-INFINITY`(如果还没有的话)
**验收标准**:
1. causal mask 中被遮蔽的值为 `-inf`(而非 `-1e9`
2. 编译通过attention test 通过
---
### FIX-20: LayerNorm 数值稳定性 [P1-Bug] ❌新发现
**问题**: `csrc/normalization/layernorm.cu:19-25` 注释写 "Welford online" 但实际用 `E[x²] - E[x]²`,大均值小方差时会灾难性抵消。
**修复要求**:
- 改为真正的 two-pass 或 Welford online 算法
- pass 1: 求 mean; pass 2: 求 variance = E[(x-mean)²]
**验收标准**:
1. 对 mean=1e6, std=1e-3 的输入layernorm 输出与 PyTorch 一致relative error < 1e-3
2. 编译通过现有测试通过
---
## FIX-06: 修复 `unsqueeze` stride 计算 [P1-Bug]
### FIX-21: LayerNorm/RMSNorm 最小 block size [P1-Bug] ❌新发现
**问题**: `tensor.rs:128` 中 unsqueeze 的 stride 计算错误。对 `[3,4]` strides `[4,1]` 做 `unsqueeze(0)` 得到 strides `[4,4,1]`,而正确应为 `[12,4,1]`。虽然 size-1 维度的 stride 不影响寻址,但导致 `is_contiguous()` 误判为 false触发不必要的 copy。
**问题**: `layernorm.cu:88` `rmsnorm.cu` hidden_size < 32 的输入会崩溃block_reduce 需要至少一个完整 warp)。
**修复要求**:
- size-1 维度的 stride 应设为 `shape[dim+1] * strides[dim+1]`(如果 dim 不是最后一维),使其满足 contiguous 条件
- 或者更简单: unsqueeze 后如果原 tensor 是 contiguous 的,直接重算 contiguous strides
- launch `block = max(min(hidden_size, 1024), 32)`
**验收标准**:
1. 单元测试: `[3,4]` contiguous tensor 做 `unsqueeze(0)` 后 `is_contiguous()` 返回 true
2. 单元测试: `[3,4]` contiguous tensor 做 `unsqueeze(1)` 后 `is_contiguous()` 返回 true
3. 单元测试: `[3,4]` contiguous tensor 做 `unsqueeze(2)` 后 `is_contiguous()` 返回 true
4. 编译通过,现有测试通过
1. hidden_size=16 layernorm/rmsnorm 不崩溃
2. 编译通过
---
## FIX-07: 使用 Caching Allocator [P1-性能]
## 第三批P2 — 重要改进
**问题**: `CachingAllocator` 已实现但从未使用。所有 GPU 分配直接 `cudaMalloc`。
### FIX-22: Engine dummy KV cache 分配 [P2-性能] ❌新发现
**问题**: `engine.rs:142-148` 每次 batched decode `std::mem::replace` 创建 dummy `GpuKVCache::new(..., 1, ...)` 来绕过 borrow checker每步分配 `num_layers * 2` GPU buffer
**修复要求**:
- 创建一个全局或 thread-local `CachingAllocator` 实例
- `Tensor::zeros` 等分配路径通过 caching allocator
- 或者至少: `GpuKVCache::get_kv_len` 中的临时 buffer 分配通过 caching allocator这是最热的分配路径
- `GpuBuffer::Drop` 需要与 allocator 配合return to pool 而非 cudaFree
- `running` `Vec<Sequence>` 改为存储方式让 KV cache 可以独立借出
- 或使用 `Option<GpuKVCache>` + `.take()` / `.insert()` 避免 dummy 分配
**验收标准**:
1. 在 decode loop 中连续调用 `get_kv_len` 100 次,`AllocStats.cuda_malloc_count` < 10大部分命中 cache
2. 编译通过,现有测试通过
1. batched decode 路径不再分配 dummy KV cache
2. 编译通过功能不变
---
## FIX-08: 修复 `CudaDeviceProp` FFI 安全性 [P1-Bug]
### FIX-23: RoPE cache 硬限 8192 [P2-功能] ❌新发现
**问题**: `ffi.rs:31` 使用 `_pad: [u8; 4096]` 假设 cudaDeviceProp 总大小。CUDA 12.9 的实际结构可能更大。
**问题**: `qwen3.rs:45` `config.max_seq_len().min(8192)` 人为截断
**修复要求**:
- 删除 `CudaDeviceProp` struct或仅保留 name 字段所需的最小 struct
- 如果只需要 name: 分配一个足够大的 buffer如 `[u8; 8192]`)并直接读取 name offset前 256 bytes
- 或者更安全: 使用 `cudaDeviceGetAttribute` + 单独的 name 查询 API`device.rs` 已经用 getAttribute 查其他属性了,只差 name
- 去掉 `.min(8192)`或改为与 engine max_seq_len 一致
- 确保 RoPE cache 覆盖实际使用的 max_seq_len
**验收标准**:
1. 不再有 `CudaDeviceProp` struct或 padding 大小基于 `std::mem::size_of` 动态确定
2. `device_info()` 仍能返回正确的 device name
3. 编译通过,现有测试通过
1. RoPE cache 长度 >= engine max_seq_len
2. 编译通过
---
## FIX-09: 修复 Tokenizer byte_fallback panic [P1-Bug]
### FIX-15: GPT-2 消除 CPU round-trip [P3-性能] ❌未修
**问题**: `bpe.rs:173-176` 中 Qwen3 tokenizer 遇到不在 vocab 的单字节时 panic
**修复要求**:
- 当 `byte_fallback == true` 且单字节不在 vocab 时,查找 `<0xNN>` 格式的 special token
- 如果 `<0xNN>` 也不存在,才 panic带有明确的错误信息
**验收标准**:
1. 使用 Qwen3 tokenizer encode 包含所有 256 个字节值的字符串不 panic
2. encode 后 decode 回来的字节序列与原始一致
3. 编译通过
**问题**: GPT-2 `split_qkv``merge_heads``add_bias` 全在 CPU 做。优先级低GPT-2 不是主力模型)
---
## FIX-10: 实现 SSE Streaming [P2-功能]
## 修复依赖图和执行顺序
**问题**: API 只支持阻塞式响应,不支持 SSE streaming。
```
第一批 P0 (可并行):
FIX-01 (cuBLAS handle) ← 独立
FIX-16 (EOS 泄漏) ← 独立
FIX-17 (max_seq_len) ← 独立FIX-23 依赖此
FIX-18 (max_tokens 校验) ← 依赖 FIX-17需要知道 max_seq_len
**修复要求**:
- `ChatRequest` 增加 `stream: Option<bool>` 字段
- 当 `stream == true` 时,返回 `text/event-stream` content type
- 每生成一个 token 发送一个 SSE event格式与 OpenAI 兼容:
```
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"token"},"finish_reason":null}]}
```
- 最后发送 `data: [DONE]`
- 非 streaming 模式行为不变
第二批 P1 (可并行):
FIX-07 (caching allocator) ← 独立
FIX-08 (CudaDeviceProp) ← 独立
FIX-09 (byte_fallback) ← 独立
FIX-19 (causal mask -inf) ← 独立
FIX-20 (layernorm 稳定性) ← 独立
FIX-21 (min block size) ← 独立
**验收标准**:
1. `curl` 请求 `stream: true` 能看到逐行 SSE 输出
2. 每行 SSE data 是合法 JSON包含 `choices[0].delta.content`
3. 最后一行是 `data: [DONE]`
4. 非 streaming 请求仍正常工作
5. 编译通过
---
## FIX-11: 修复 Usage 统计 [P2-功能]
**问题**: API 返回的 usage 全是 0。
**修复要求**:
- 追踪 prompt token 数量和 completion token 数量
- 在 non-streaming 响应中返回正确的 usage
- 在 streaming 最后一个 chunk或 `[DONE]` 前)可选择性包含 usage
**验收标准**:
1. 发送一个 non-streaming 请求,`usage.prompt_tokens` > 0`usage.completion_tokens` > 0
2. `usage.total_tokens == usage.prompt_tokens + usage.completion_tokens`
3. 编译通过
---
## FIX-12: `GpuKVCache::get_kv_len` 避免重复分配 [P2-性能]
**问题**: 每次调用 `get_kv_len` 都 `GpuBuffer::alloc` 新内存decode 循环中每步每层一次。
**修复要求**:
- 方案 A: 返回 view/slice 到已有的预分配 buffer零分配需要构造 Tensor 时使用正确的 strides 指向 padded buffer
- 方案 B: 在 GpuKVCache 中预分配 output bufferget_kv_len 做 D2D copy 到固定 buffer每层 2 个 output buffer
- 方案 A 更优但实现复杂度更高
**验收标准**:
1. 连续调用 `get_kv_len` 100 次,`cudaMalloc` 调用次数 <= 2初始分配
2. 返回的 tensor 数据正确(与修改前 bit-exact
3. 编译通过,现有测试通过
---
## FIX-13: 实现 Sampling Strategies [P2-功能]
**问题**: 只有 greedy sampling没有 temperature / top-k / top-p。
**修复要求**:
- 实现 `SamplingParams { temperature, top_k, top_p }` struct
- temperature: `logits = logits / temperature` 后 softmax 后按概率采样
- top_k: 保留 top-k logits其余置 -inf
- top_p: 按概率降序累加到 >= p 后截断
- greedy 作为 `temperature = 0` 或独立模式
- `GenerateRequest` 接收 sampling params
- API 层解析 temperature / top_k / top_p 参数
**验收标准**:
1. temperature=0.0 与 greedy 结果一致
2. temperature=1.0 多次生成同一 prompt 产生不同结果
3. top_k=1 与 greedy 结果一致
4. 编译通过
---
## FIX-14: GPU Tensor contiguous() 用 GPU kernel [P2-性能]
**问题**: `tensor.rs:148` 中非 contiguous GPU tensor 做 contiguous 需要 GPU→CPU→CPU copy→CPU→GPU。
**修复要求**:
- 实现一个通用的 strided copy GPU kernel或至少对常见的 transpose 情况有 kernel
- `contiguous()` 对 GPU tensor 直接在 GPU 上完成
**验收标准**:
1. 对一个 GPU 上的 transposed tensor 调用 `contiguous()`,不触发任何 `cudaMemcpy` H2D/D2H
2. 结果与 CPU 实现 bit-exact
3. 编译通过,现有测试通过
---
## FIX-15: GPT-2 消除 CPU round-trip (split_qkv, merge_heads, add_bias) [P3-性能]
**问题**: GPT-2 的 `split_qkv`, `merge_heads`, `add_bias` 全在 CPU 上做。
**修复要求**:
- `add_bias`: 实现 broadcast-add GPU kernel[S,N] + [N] → [S,N]
- `split_qkv`: 实现 GPU kernel 将 [S, 3H] 分成 Q/K/V 并 reshape 为 [1, heads, S, D]
- `merge_heads`: 复用已有的 `merge_heads_gpu` kernel目前只有 BF16 版本,需要 F32 版本)
**验收标准**:
1. GPT-2 forward path 中 `grep -n "to_device(Device::Cpu)"` 只出现在 `sample_greedy` 中
2. 推理结果与修复前一致greedy decode bit-exact
3. 编译通过,现有测试通过
---
## 修复优先级排序
**第一批 (必须先做,其他依赖它们)**:
1. FIX-01: 全局 cuBLAS handle
2. FIX-02: 移除 device sync
3. FIX-03: Chat template
4. FIX-04: is_finished EOS
**第二批 (重要 bug 修复)**:
5. FIX-05: Storage device tracking
6. FIX-06: unsqueeze stride
7. FIX-08: CudaDeviceProp
8. FIX-09: byte_fallback panic
**第三批 (功能完善)**:
9. FIX-10: SSE streaming
10. FIX-11: Usage stats
11. FIX-13: Sampling strategies
**第四批 (性能优化)**:
12. FIX-07: Caching allocator
13. FIX-12: KV cache alloc
14. FIX-14: GPU contiguous
15. FIX-15: GPT-2 CPU round-trip
第三批 P2:
FIX-22 (dummy KV cache) ← 独立
FIX-23 (RoPE cache) ← 依赖 FIX-17
```