系列:《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 篇 · 总纲(阿里云社区)
上一篇:19-2《vision_tokens 客户端预编码协议》| 下一篇:20-1《极简 HTTP:socket/bind/select 轮询》
工程边界篇(H.264 未完成):MP4/H.264 媒体解码为独立模块、默认构建不启用、功能未完成;本文不演示、不编造其行为与数据
一句话导读:推理引擎里 H.264 媒体模块的工程边界:MP4/H.264 解码是独立模块、默认构建不启用、功能未完成,本篇只读其 API 面与设计取舍,不演示、不编造任何行为与数据。
关键词:手搓 Qwen 推理引擎、千问大模型推理、Qwen3-VL、零依赖纯 C、H.264、独立模块、MP4、媒体解码、工程边界
导语:视频这条线其实分两层:帧序列路径已实现并实测,而 MP4/H.264 解码是独立模块、默认构建不启用、尚未完成。本篇不演示也不编造它的行为,只带你读它的 API 面与工程取舍,看清一门未完成模块在项目里的诚实标注方式。
Day 19 前两篇把"图片 → 视觉 token"讲完了。视频这条线要分成两层现实说清楚,避免把工程文档里没有的东西讲成"已经能跑":
- 帧序列路径(已实现、已实测):引擎收"RGB 帧数组"(
video_frames协议或帧目录),沿时间维 patch 后走与图片相同的 ViT——19-1 实测 4 帧 64×64 → 8 个视觉 token; - MP4/H.264 解码路径(独立模块、未完成、默认构建不启用):
include/media/vllm_media.h定义了 MP4 容器解析 + 自研 H.264 软件解码器 + YUV→RGB 的 API 面,src/media/下有vllm_mp4.c(276 行)与h264_dec.c(2031 行)的骨架实现,但不在默认构建里(CMakeLists 的 28 个 C 源文件不含它,docs/技术文档.md的模块树也明确标注),且当前连独立语法检查都过不了(板端实测,见 §3)。按项目诚实口径:本系列不演示、不编造该模块的行为与数据。
1. 知识点:视频多模态的"解码"与"编码"是两件事
把"看视频"拆开,是三步完全不同的工程:
MP4 文件 ──①容器解析──> H.264 码流 ──②解码──> YUV 帧 ──③上色/缩放──> RGB 帧
│
st_vision_encode_video
▼
(temporal patch) → 视觉 token
- ① 是容器层:MP4/ISO-BMFF 的 box 结构、sample 表、SPS/PPS 参数集(
MPMovie结构里存的都是这些); - ② 是视频编解码层:H.264 的 NAL、slice、宏块、CABAC/CAVLC——这是视频领域公认的"工程量杀手";
- ③ 是像素层:YUV420 → RGB + 缩放,普通图像代码就够。
关键认知:视觉编码(ViT,19-1 那 295ms)只关心最后一步之后的事——它吃 RGB 帧,完全不关心帧是从 mp4 解出来的还是客户端上传的。所以引擎把"①②③"与"ViT"解耦:video_frames 协议(19-2 §2.3)直接从"③之后"开始,把 ①②③ 留给了媒体模块去解决。这就是"为什么视频模块没做完,视频多模态链路依然能跑一半"的结构性原因。
引擎给 H.264 解码器定的抽帧策略(include/media/vllm_media.h 第 6 行注释)是"从最近 IDR 解码到目标帧"——不解全片只解你抽的那几帧,这是视频问答场景(抽帧后逐帧过视觉塔)最省算力的设计,与全片转码是两种目标。
2. 对应代码:只读任务——模块长什么样
2.1 API 面(include/media/vllm_media.h 全文可读)
头文件声明了三组接口:
/* MP4 容器解析 (demux) */
typedef struct MPMovie {
… } MPMovie; /* sample 表、SPS/PPS、帧索引 */
int mp4_open(MPMovie *m, const uint8_t *data, size_t size); /* 50 行 */
void mp4_close(MPMovie *m);
/* H.264 软件解码器 */
typedef struct H264Dec H264Dec;
int h264_decoder_init(H264Decoder *d, const MPMovie *movie); /* 74 行 */
int h264_decoder_decode(H264Decoder *d, int frame_idx); /* 82 行 */
/* 帧处理:YUV420 → RGB 缩放 */
int media_yuv_to_rgb(const uint8_t *yuv, int src_w, int src_h,
uint8_t *rgb, int max_edge, int *dst_w, int *dst_h);
读它的方式(B 档任务会带你做):先只看结构体字段与行号注释,不看实现。MPMovie 里 sample_off/sample_size/sample_pts_ms/sample_idr 就是"容器层要回答的问题"——一个 mp4 里哪些字节是一帧、这帧是不是 IDR、显示时间戳多少。
2.2 与"已实现"层的对照
把 include/media/vllm_media.h(H.264)和 include/model/vllm_media.h(图片/帧)并排读,是理解项目分层的捷径:
| model/vllm_media.h(已实现,编进默认构建) | media/vllm_media.h(独立模块) | |
|---|---|---|
| 作用 | 图片解码(stb_image 内嵌)+ base64 + 帧目录 | MP4 解析 + H.264 解码 + YUV→RGB |
| 默认构建 | 是(src/model/vllm_media.c,CMakeLists 第 73 行) |
否(src/media/ 不在 28 个 C 源里) |
| 状态 | 19-2 已实测(image_url 解码 + video_frames 帧数组) | 未完成,见 §3 实测 |
3. 改动后果:把它"纳入构建"会发生什么
实测口径:板端 RK3588 / aarch64 / 2026-09-07。方法:
gcc -fsyntax-only对src/media/两个文件做独立语法检查(-Iinclude -Iinclude/model -Iinclude/media -Iinclude/common)。这是"尝试纳入构建"的最小动作——只查语法,不链接不运行,不会编造任何运行行为。
-- vllm_mp4.c -- (276 行)
src/media/vllm_mp4.c:51:14: error: unknown type name ‘MPMovie’
src/media/vllm_mp4.c:273:16: error: unknown type name ‘MPMovie’
-- h264_dec.c -- (2031 行)
src/media/h264_dec.c:278:11: error: unknown type name ‘MPMovie’
src/media/h264_dec.c:1124:23: error: unknown type name ‘H264Decoder’
src/media/h264_dec.c:1172:24: error: unknown type name ‘H264Decoder’
src/media/h264_dec.c:2021:25: error: unknown type name ‘H264Decoder’
观测到的现状(如实陈述,不做推测性归因):
- 两个文件都
#include "vllm_media.h",但当前 include 次序下解析到的是include/model/vllm_media.h(图片版),而不是声明MPMovie/H264Decoder的include/media/vllm_media.h——同名头文件在两个目录、且 include 顺序先把 model 版纳入,MPMovie、H264Decoder因此"未知"; - 即便修正头文件解析,两个头文件还共用同一个 include guard 名(
VLLM_MEDIA_H),同一编译单元同时引入会互相吞并——这是"模块还没整理到可编译状态"的直接信号; - 与仓库文档完全一致:
docs/技术文档.md§2.1 的目录树(第 75 行)与编译单元统计(第 81–83 行,共 28 个 C 源文件)明确写着media/的vllm_mp4.c/h264_dec.c不在默认构建。
结论(诚实版):该模块定义了清晰的 API 面与"最近 IDR 抽帧"策略,代码有骨架(276 + 2031 行),但尚未达到可纳入默认构建、可演示运行的状态。本系列对它的处理方式是:只读 API、读懂设计取舍,不演示、不引用任何未实测的性能或行为数字。等它完成并合入默认构建后,若教程还在更新,再补真机篇——在那之前,任何声称"H.264 已可用"的说法都是越界。
4. 学员调试任务
- A 档(板端/本地动手):
- 复刻 §3 的语法检查:对
src/media/vllm_mp4.c与h264_dec.c跑gcc -fsyntax-only,记录错误清单(预期是MPMovie/H264Decoder unknown type一类); - 把
-Iinclude/media提到-Iinclude/model前面再跑一次,观察错误是否变化——用实验回答"同名头文件 + include 顺序"到底影响什么; - 读
include/media/vllm_media.h的MPMovie字段表,对着h264_dec.c的h264_decoder_decode(2021 行附近)注释,画出"抽一帧要经过哪些步骤"的流程图(只画结构,不跑代码)。
- 复刻 §3 的语法检查:对
- B 档(纯读源码):对比
include/model/vllm_media.h与include/media/vllm_media.h,回答:① 两个头文件为什么会出现同名?这种命名在模块独立开发期是常见现象还是应该避免?(工程判断题,无标准答案)② 如果将来把src/media/纳入构建,CMakeLists.txt的源文件清单、vllm_server.c的请求分发、以及docs的"28 个 C 源文件"统计,哪些地方必须同步改?③ 为什么"不解全片、从最近 IDR 解到目标帧"对视频问答场景是对的取舍(提示:对比全片转码的内存/时间成本与抽帧问答的实际需求)?
预期输出:一份"媒体模块现状核对单"——API 面 ✓、骨架文件 ✓、默认构建 ✗、独立语法检查 ✗、可演示运行 ✗(无实测可引),外加一张"mp4 → 帧"的抽帧流程图。
收尾
- 本篇源码点名:include/media/vllm_media.h(API 面 + 抽帧策略注释)、src/media/vllm_mp4.c 与 src/media/h264_dec.c(骨架实现,不在默认构建)、技术文档.md §2.1(模块树与 28 源文件统计)
- 开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)
- 下篇预告:多模态数据面收官,Day 20 开始服务化第一课——自研 HTTP。socket/bind/select 轮询怎么做、为什么引擎不引 libevent/nginx 也能撑住板端服务,
curl /health背后发生了什么。