0.8MB 跑通 Qwen|第 20-3 篇:SSE 流式——推理引擎响应怎么写一半就发给客户端

简介: 本系列《0.8MB跑通Qwen》用纯C手搓零依赖推理引擎,适配Qwen3-VL多尺寸模型,在RK3588(aarch64)实测SSE流式响应:首token延迟130ms,后续约50ms/token,支持标准+自定义事件,真正实现端侧“打字机”体验。(239字)

系列:《0.8MB 跑通 Qwen:从零实现 ARM 零依赖纯 C 推理引擎》(30 天 × 90 篇) | 适配模型:Qwen3-VL-8B-Instruct(千问3_VL_8B_Instruct)· Qwen3-VL-2B-Instruct · Qwen3-30B-A3B | 测试设备:RK3588(4×Cortex-A76 + 4×Cortex-A55,aarch64)

系列总纲:《0.8MB 跑通 Qwen》30 天 90 篇 · 总纲(阿里云社区)

上一篇:20-2《路由与自研最小 JSON》| 下一篇:21-1《/v1/chat/completions:兼容协议的最小面》

真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)

一句话导读:推理引擎的 SSE 流式响应:状态行与头先发,事件流按 token 逐条推送直到 [DONE],板端实测首事件 130ms、此后每 token 约 50ms,看响应怎么写一半就发给客户端。

关键词:手搓 Qwen 推理引擎、千问大模型推理、Qwen3-VL、零依赖纯 C、SSE、流式响应、HTTP、逐 token、[DONE]

导语:模型边生成、客户端边看到字,靠的是 SSE:状态行与响应头先发,事件流按 token 逐条推,直到 [DONE] 关流。本篇拆开流式三件套,并在 RK3588 上用带时间戳的客户端视角,看首事件与逐 token 到达的节拍。

对话体验的最后一公里是"打字机效果":模型边生成,客户端边看到字。HTTP 本身是请求-响应,要"分片到达"得靠 SSE(Server-Sent Events)。这篇拆 vllm_http.c 的流式三件套,并贴板端实测:stream:true 请求在 130ms 收到首事件、此后每个 token 约 50ms 一个 chunk,直到 [DONE]。

1. 知识点:SSE 到底是什么

SSE 不是协议魔法,它是"HTTP 响应体按约定格式分片写":

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

data: {json 片段1}\n\n
data: {json 片段2}\n\n
data: [DONE]\n\n

规则就三条:状态行+头先发出去;之后每个事件是 data: … 行、以空行结尾;连接保持打开直到流结束。客户端(fetch/EventSource/curl)按"空行切事件"解析即可——不需要任何 WebSocket。对本引擎,SSE 与推理的配合方式是(Day 14 单机守则的延续):一次只服务一个生成流,流里逐 token 调前向、逐 token 推事件,worker 不并行抢推理。

2. 对应代码:流式三件套

vllm_http.c 的接口就三个函数(666–690 行):

int vhttp_stream_begin(VHttpConn *conn, int status, const char *content_type); // 666
int vhttp_stream_write(VHttpConn *conn, const char *data, size_t len);         // 678
int vhttp_stream_flush(VHttpConn *conn);                                       // 681
int vhttp_stream_done(VHttpConn *conn);                                        // 685

实现要点:begin 先发状态行与头;write 逐事件写;flush 是空实现(683 行注释:socket 无缓冲,每次 write 都到线缆)——这是"最小实现"里少有的"因为不做缓冲所以天然实时"的巧合,值得在源码里体会。流式端点(/v1/chat/completions?stream=true)在生成循环里每出一个 token 就 vhttp_stream_write 一个 data: 事件,末尾补 data: [DONE]。SSE 事件里还夹了两个"非 OpenAI 标准"的自定义事件:chat.completion.metrics(逐段时序)与 chat.completion.tokens(本轮完整 token 列表)——标准之外的扩展要标清楚,客户端不认识就跳过(框架的宽容度在这里是特性)。

3. 改动后果:curl 视角的逐 token 到达

实测口径:板端 serve + x86 客户端,POST /v1/chat/completions {"stream":true, "messages":[...从1数到20...], "max_tokens":30},2026-09-07。客户端按行读事件并打时间戳。

t=  130.3ms  data: {...delta role:"assistant"...}     ← 首事件(角色帧)
t=  930.4ms  data: {…delta content 第 1 个 token…}
t= 1061.1ms  data: {…第 2 个 token…}
t= 1191.8ms  data: {…第 3 个 token…}
   …(此后每个事件间隔 ≈ 50–60ms,稳定)
t= 2726.5ms  data: {…第 N 个 token…}
t= 2727.3ms  data: {"object":"chat.completion.metrics","metrics":{…}}
t= 2727.6ms  data: {"object":"chat.completion.tokens","tokens":[…]}
t= 2727.7ms  data: [DONE]
→ first_chunk=130.3ms  total_chunks=35  elapsed=2728ms

可以量化的三点:

  1. 首事件 130ms 就到:在 decode 出第一个字之前,客户端已经拿到"流开始了 + role 帧"——这让前端能立刻把光标切到打字状态,体感延迟被隐藏;
  2. 逐 token ≈ 50–60ms:与 Day 19-2 的 tpot_ms≈55 一致——SSE 的节拍就是 decode 的节拍,流式没有给推理额外加可见延迟(write 空缓冲是原因之一);
  3. 结束三连:metrics → tokens → [DONE] 在 1ms 内收尾,[DONE] 是客户端"关流"的信号。

诚实备注:SSE 的实时性在局域网里测是干净的;跨公网时 TCP 拥塞/代理缓冲会破坏"逐 token"观感(有些中间代理会攒包),这是 SSE 的已知边界,不属于引擎能解决的范畴。

4. 学员调试任务

  • A 档(本地动手):
    1. 用 curl -N -X POST … -d '{…"stream":true…}' 观察逐行事件到达(-N 关缓冲是关键),记录每两个 data: 的间隔;
    2. 把 max_tokens 调到 200,观察事件间隔是否仍稳定在 ~50ms(推理慢时 SSE 节拍会拉长——把"流式节拍 = 解码速度"这个等式亲自验证一遍);
    3. 找一个会吞 SSE 的代理场景(如 Python http.server 反代或 nginx 默认缓冲)复现"攒包",理解 X-Accel-Buffering: no 这类头存在的意义。
  • B 档(纯读源码):读 vllm_http.c 666–690 与生成循环里写事件的调用点,回答:① 为什么 flush 可以空实现而 write 不能省?(提示:谁负责把字节推到线缆)② [DONE] 之后连接是关还是复用,读 vhttp_stream_done 与调用方的收尾逻辑;③ 若某天改成"攒 4 个 token 再推一次",对 ttft 观感与 metrics 里的 ttft_ms 各有什么影响?

预期输出:一张你自己板子的"流式事件时间轴"(含首包与逐 token 间隔),并说明 [DONE] 前的三个自定义事件各是什么用途。

收尾

  • 本篇源码点名:vllm_http.c(vhttpstream* 666–690)、vllm_server.c(流式对话 handler、metrics/tokens 自定义事件)
  • 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
  • 下篇预告:传输层全会了。Day 21 进"业务层":/v1/chat/completions 的请求字段怎么映射成引擎内部的 prompt 与参数,OpenAI 兼容的"最小面"到底有多大。
相关文章
|
1天前
|
编解码 缓存 计算机视觉
0.8MB 跑通 Qwen|第 19-1 篇:ViT 编码——推理引擎里图片是怎么变成视觉 token 的
本系列《0.8MB跑通Qwen》用纯C手写零依赖推理引擎,实现在RK3588(aarch64)上部署Qwen3-VL多模态模型。真机实测:64×64图经24层ViT编码得4个视觉token,耗时仅295.3ms,支持图文理解与视频分析。(239字)
|
1天前
|
算法 定位技术 C语言
0.8MB 跑通 Qwen|第 16-3 篇:推理引擎的 tensor 目录与布局重排的落盘顺序
《0.8MB跑通Qwen》系列实录:30天手搓零依赖纯C推理引擎,适配Qwen3-VL多模态模型(2B/8B/30B-A3B),真机跑通RK3588(aarch64)。详解VQFTensor目录结构、64字节对齐落盘、FNV校验及mmap加载机制,含零依赖Python解析器。
|
1天前
|
C++
0.8MB 跑通 Qwen|第 10-3 篇:推理引擎里诚实的坑——哪些场景稀疏无收益,甚至负优化
本篇实测揭示稀疏注意力的真相:短上下文(<1K)开`--sparse-attn`反成负优化!RK3588板端验证,详列8条“禁用清单”,每条附源码行号与实测数据。明确边界:q4 KV、推测解码、L3驱逐等均与稀疏互斥,并坦承`test-sparse`自检崩溃缺陷。(239字)
|
1天前
|
存储 缓存 C++
0.8MB 跑通 Qwen|第 9-1 篇:推理引擎的 KV 也量化——q8 KV 把缓存与 decode 带宽压到一半
本系列手搓零依赖纯C推理引擎,仅0.8MB即可在RK3588上跑通Qwen3-VL多模型。本文详解q8 KV量化:将f16 KV压至INT8,按token每头独立定标,缓存与decode带宽降低约48%,兼顾精度与效率,f32仅作调试探针。
|
1天前
|
存储 容器
0.8MB 跑通 Qwen|第 17-2 篇:GGUF → VQF——推理引擎的 Q4_0…Q8_K 反量化再量化
本系列《0.8MB跑通Qwen》聚焦ARM零依赖纯C推理引擎,适配Qwen3-VL多版本模型,在RK3588平台实测。本文详解GGUF→VQF的“反量化再量化”路径:将llama.cpp已量化权重(Q4_0/Q8_0等)先还原为F32,再经统一量化与重排固化为VQF格式,确保内核兼容性与可验证性。(239字)
|
1天前
|
缓存 NoSQL 区块链
0.8MB 跑通 Qwen|第 15-3 篇:推理引擎单步走一次生成——token 循环与 KV 追加
本系列《0.8MB跑通Qwen》用纯C手搓零依赖推理引擎,实测RK3588板端运行Qwen3-VL多模型;本文以gdb单步追踪decode循环,直观展示“采样→前向→KV追加→停判”全过程,将大模型逐token生成钉在胶片上。(239字)
|
1天前
|
调度 C++
0.8MB 跑通 Qwen|第 7-2 篇:推理引擎的混合精度路由——flags 决定谁用 Q8、谁用 Q4
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型,在RK3588上实现实测。核心创新是混合精度路由:prefill阶段用Q8保精度,decode阶段用Q4省带宽,通过flags比特位动态调度,兼顾速度与质量。(239字)
|
1天前
|
开发工具 C++ git
0.8MB 跑通 Qwen|第 5-3 篇:推理引擎的参考实现对拍——怎么读懂 rel err 的数量级
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎开发,实测适配Qwen3-VL多模型,在RK3588上完成Q8/Q4量化与NEON加速。核心创新在于“两把尺子”误差分析法:内核一致性(1e-7)验证手写代码正确性,量化损失(1e-2)界定精度边界,实现可验证、可调试、可落地的轻量级LLM部署。(239字)
|
1天前
|
API 调度 C++
0.8MB 跑通 Qwen|第 14-1 篇:大模型推理的连续批处理——iteration-level 调度思想
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型,在RK3588上实测落地。本文详解iteration-level连续批处理:动态聚合多请求的decode步,单次前向服务多人,兼顾并发与确定性。(239字)
|
1天前
|
安全 索引
0.8MB 跑通 Qwen|第 12-2 篇:推理引擎的磁盘 KV 快照格式——0.47 GB 从哪来
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,实测RK3588上部署Qwen3-VL多模态模型。本文详解0.47GB磁盘KV快照格式:64字节头+token IDs+28层F32原样KV,逐字段对账到字节级,诠释“宁大勿错”的可靠性设计。(239字)

热门文章

最新文章