Redis 之父下场手写推理引擎:antirez 用约 24000 行原生代码驯服视频生成模型
posts posts 2026-08-16T22:00:00+08:00antirez 的新项目 h3-metal:不依赖 PyTorch、不套 MLX,约 24000 行 C、Objective-C 与 Metal shader 在 Apple Silicon 上跑通 MiniMax-H3 视频/音频生成。本文拆解它的 vertical slice 工作流、speed/quality 参数空间和 SSD streaming 的工程折衷,所有数字对照仓库 README 与源码逐条核实。技术笔记AI 推理, Apple Silicon, Metal, C 语言, MiniMax-H3Redis 之父下场手写推理引擎:antirez 用约 24000 行原生代码驯服视频生成模型
写出 Redis 的那个人,最近把一个新仓库推上了 GitHub:antirez/h3.c,项目名叫 h3-metal。打开仓库第一眼看不到 PyTorch,看不到 MLX,甚至看不到 Python——约 24000 行原生代码,拆开是 15 个 C 模块、4332 行 Metal shader(Metal 的 GPU 内核语言,地位类似 CUDA kernel)和 4711 行 Objective-C 写的 Metal 运行时。目标只有一个:在 M3 Max / M5 Max 这类 Apple Silicon 机器上,把 MiniMax-H3——一个 33B 参数、能同时生成视频和原生立体声音频的全模态模型——跑进可交互的速度。默认输出 864×480、56 帧、24 fps,一个 binary 加一个 .metal 文件,编完就能用。
这件事值得写,和「名人又发新项目」无关——它几乎反着当下所有推理框架的直觉走:不建框架,不切算子库,不做后端抽象层,一个人按垂直切片(vertical slice)一路推到底,每一片都能独立跑通、独立验证、独立交付。在「推理引擎」这个词已经和「几百万行代码加几十号维护者」绑定的 2026 年,这个仓库安静得像一封异议书。读它,更像在读一份老 C 程序员写给推理工程师的工程笔记——「为什么这么做」的篇幅和「怎么做」一样长。
目录
- 不是框架,是一根打通的管子
- Vertical slice:一片片切出来的引擎
- speed/quality 参数空间:把妥协做成显式参数
- SSD streaming:84% 慢换 95% 内存省
- 交互会话:Iris 风格的 ! 命令
- 测试分层:四条不同的验收线
- 写给推理工程师的几句话
- 动手练习与下一步
- 参考
不是框架,是一根打通的管子
先把它和常见选项区分开。llama.cpp 走的是「计算图 + 多后端」路线,MLX 走的是「NumPy 式数组 + 惰性求值」路线,两者的前提都是:模型会变,算子要通用,所以先建一座通用桥,让不同的模型都能从桥上走过去。h3-metal 的前提相反:模型就一个(H3-Base,768p,24 fps),硬件就一类(Apple Silicon 的 GPU 加统一内存),那就没必要建桥——直接铺一根从权重文件到 mp4 的管子,管子的每一段都为这一个模型、这一类硬件量身裁。
管子的构成一眼能看清,16 个核心源文件各司其职。h3.c(1731 行)是总装线;h3_safetensors.c(584 行)负责读 safetensors 权重;h3_host.c(647 行)负责 host 侧的确定性调度;h3_dit.c(3151 行)是 DiT(Diffusion Transformer,即扩散模型里的 Transformer 架构主干,负责逐步去噪)的完整前向,全仓库最大的 C 文件;h3_video_vae.c(1252 行)和 h3_audio_vae.c(1343 行)各管一头的 VAE(变分自编码器,负责像素/波形与隐空间之间的双向翻译)——视频一路、音频一路,两条路径独立实现、互不共享代码;h3_text_encoder.c(835 行)处理 prompt(提示词)编码;h3_video_encoder.c(814 行)和 h3_vision_encoder.c(633 行)伺候参考输入;h3_multimodal.c(360 行)把多模态条件缝起来;h3_ffmpeg.c(755 行)负责出片;h3_weights.c(202 行)管权重装载的生命周期;h3_cli.c(793 行)把 41 个 CLI(命令行工具)选项路由进去,也承载交互会话;h3_terminal.c(304 行)实现 Kitty、Ghostty、iTerm2、WezTerm、Konsole 等终端图形协议,负责在终端里直接预览帧——它默认按 2 倍显示尺寸输出,适配 macOS Retina 屏,非 HiDPI 屏用 --zoom 1 调回;h3_dit_schedule.c(485 行)管去噪调度。GPU 侧的活全部压进 h3_shaders.metal 那 4332 行里。
这个分工有个很 antirez 的特点:每个模块的行数都控制在一个人能完整装进脑子的量级。最大的 h3_dit.c 三千出头,其余大多在几百到一千出头。对比主流推理库里动辄上万行的融合算子文件,这里的意图很明显——他要的是随时能通读、能改、能推倒重写的代码,而不是一座敬而远之的算子大教堂。行数即认知预算,这是老 C 程序员的肌肉记忆:Redis 就是这么写成的。
Vertical slice:一片片切出来的引擎
仓库 README 里最有信息量的不是性能数字,而是开发顺序:deterministic host/metadata(确定性 host 与模型元数据)→ portable Metal parity(可移植 Metal 对等实现)→ prompt(提示词)encoding → prompt-to-video/audio → first/last-frame(首尾帧)→ ordered Ref2VA。这是一条典型的垂直切片路线——每一片都从 host 侧一直切到屏幕上的输出,切完即可验证,然后再切下一片。
这和主流 ML 工程「先搭完整框架、再逐个填功能」的做法正好相反。框架先行的代价是:前两个月没有任何能跑的东西,第一个能跑的东西出现时,bug 可能埋在任意一层,而你连先怀疑谁都不知道。切片先行的代价是另一种:每切一片都要面对「这一片就是完整系统」的现实——内存怎么算、错误怎么报、输出怎么验、和参考实现的差异算多大,全都绕不过去。antirez 选了后者。这个选择在小团队手里是风格,在单人项目里是生死:没有切片带来的持续可验证性,一个人维护两万多行涉及 GPU 的代码,几周就会失去对系统的掌控。
看它如何对待「正确性」就知道这种纪律有多硬。README 明确写了和 MLX 参考实现的对齐标准:数值像素不要求一致——随机数源和执行引擎的差异是客观存在的,强追一致是自欺欺人——描绘的内容和运动必须一致。这是一个非常老练的验收口径:它承认浮点世界没有绝对复现,把验收线划在语义层而不是 bit 层。但注意,这条宽松的线只在「跨引擎对比」时用;在自家 Metal 路径内部,标准又收紧到 byte-identical——后面讲 SSD streaming 时会看到这条严苛的线如何变成卖点。宽严各有其位,这份分寸感比任何性能数字都更能说明作者是谁。
speed/quality 参数空间:把妥协做成显式参数
推理引擎的默认参数通常是一组「作者觉得还行」的魔法数,用户要么全盘接受,要么进源码考古。h3-metal 反过来,把速度/质量的权衡摊成四个核心旋钮,全部做成 CLI 选项,并且每个档位配了验证数字:
--steps:扩散采样步数。默认 20,激进档 4–7,reference 档 50。--reuse:跨步特征复用级别。1 是 close(贴近参考),2 是 fast,3 是 aggressive。--layers:参与计算的 DiT 层数。50 是 exact,45 是 fast,40 是 aggressive。--core-reuse:核心块复用粒度,取 1/4/6。它每步刷新 patch/head,只让昂贵核心少跑——6 就是激进预览上限,更大的值在验证里丢了主体保真度,直接不暴露。
先说说这些名字。--reuse 不叫 --velocity-reuse,--layers 不叫 --active-layer-count——antirez 给旋钮起名的原则是「高频敲的东西要短」,一个要在终端里反复试的参数,每多一个音节都是摩擦。这是 Redis 命令式的老癖好:GET/SET/INCR,一个动词一个参数,敲错都难。旋钮本身是给人用的 API(应用程序接口),命名学在这里不是审美问题,是交互成本问题。
默认档是 20 steps / 50 layers / 1 reuse(README 明写 The defaults are --steps 20 --layers 50 --reuse 1),采样器用的是随模型发布的 shifted video/audio schedule。reference 档 50/1/50/1 用来产出对照组,激进档 4-7/3/40/6 用来试探极限。四个旋钮之外还有附加开关:--token-reduction(把视频 token(词元)按行两两配对、在中间层缩减序列长度的激进模式)和 --render-width/--render-height(内部画布,在更小的同比例画布上跑模型和 VAE,再用 vImage 放大到输出尺寸)。
最有说服力的一组对照来自 4-pass 极限档:fox 视频在 M5 Max 上 3.5 秒出片,reference 路径要 26.4 秒;对 29-pass reference 的 SSIM(结构相似度,越接近 1 越像参考输出)是 0.556,独立的 surfer 测试是 0.547。0.556 这个数字意味深长——它够不上「以假乱真」,但能让你看清是同一只狐狸、同一组运动轨迹。作者没有把这个不够漂亮的数字藏起来,反而写明它适用于什么(快速预览、构图迭代)、不适用于什么(最终交付)。
低预算档的调度选择也不是拍脑袋:README 记录了它和七种候选(actual-video-sigma 线性间距、二次/三次 warp、30 点尾部子集、幂 warp、零阶保持全网格速度、线性速度外推、RES)的对比——尾巴偏重的候选大多让主体锐了、运动却坏掉,或者留下编织状的重复背景纹理。
--use-int8-row-fc2(把 MLP 第二层全连接 FC2 按行量化到 int8,一行一个激活 scale)同理:完整去噪前向减少约 2.6%,4-step 下 fox/surfer 的 SSIM 分别是 0.919/0.828——狐狸几乎无损,冲浪场景掉得明显。所以文档会告诉你哪类内容别开这个开关,而不是宣称「量化无损」。0.919 和 0.828 并排躺着,这就是诚实的样子。顺带一提,M5 上默认的 MLP 引擎本身已经是 int8(分组量化),--use-int8-row-fc2 是把它换成一行的 scale 更粗放的版本。README 还记录了完整的 int8 演进路径:同一个固定 50 层、19 次过渡的 512 渲染,BF16 MPS 路径 36.30 秒,换 int8 MLP 25.80 秒,QKV 投影再量化后 19.32 秒,峰值权重驻留从 36.4 GiB 降到 25.9 GiB。
参数之间还有互斥纪律:--reuse 和 --core-reuse 完全互斥(一个是整块速度复用,一个是保留 timestep 相关 patch/head、只复用昂贵核心,两者语义冲突),层瘦身(--layers)可以和其中任意一个组合。--token-reduction 不能和 --layers 40 --reuse 3 这类激进组合共用——那个 6.47 秒的实验产生了颜色振铃、轮廓重影和鬼影肢体,尽管 latent 范数看起来还正常。
token reduction 本身的效果也拿数字说话:M5 Max 上 45 layers + reuse 2 档位从 16.69 秒降到 12.60 秒;在 19 次前向的 A/B 里把完整去噪从 39.13 秒压到 28.06 秒(28.3%),代价是构图可能偏离 close 路径,所以它默认关着。
还有一个少见的细节:native 256 RoPE(旋转位置编码,给 token 序列注入位置信息的机制)会在 256 正方形画布上自动把空间 RoPE 坐标减半,消掉画面上的网格状伪影;而 native 128 直接标注「不支持」——4×4 的 token 网格恢复不出主体,作者宁愿砍掉这个入口,也不留一个会默默产生垃圾输出的开关。
硬约束同样显式:宽高必须是 32 的倍数(至少 32),乘积不超过 768×1344(H3-Base 是 768p 模型);帧数必须是 5 + 17*n(VAE 时间压缩比与首尾帧对齐的产物),--seconds 10 会向上取整成 243 帧(10.125 秒);帧率锁 24 fps。这些不是「建议」,是模型结构和硬件对齐后的硬边界,写在参数校验里,越界直接报错,不给你看到畸形结果再猜原因的机会。
SSD streaming:84% 慢换 95% 内存省
整个仓库里我最喜欢的一笔工程判断是 --ssd-streaming。
背景先说清楚:DiT 的 BF16 权重有约 36.5 GiB(tracked tensor storage,即 DiT 各层的 QKV、FC1、FC2 权重驻留),加上 prompt 编码器、两个 VAE、preview 解码器,统一内存再大也架不住同场抢地盘。M5 的默认做法是零拷贝:把 safetensor shard 直接 mmap 成 Metal buffer,让 37 GiB 的模型保持 file-backed、可回收。
SSD streaming 再进一步——它要求 DiT 权重几乎全部不驻留:每层只留小的 normalization 权重,两个完整的 BF16 矩阵槽交替使用,后台线程按 checkpoint 顺序预取下一个 block,GPU 跑当前 block 的同时下一块已经在路上。读取用显式的 pread 加 F_NOCACHE(Darwin uncached read),读完即弃,SSD 上不残留 page cache(页缓存)副本,实测内部 SSD 吞吐 13–14.6 GiB/s。结果:DiT 权重驻留从 36.5 GiB 降到 2.0 GiB(512 正方形)和 2.1 GiB(864×480)。
为什么不干脆交给 mmap 和内核 page cache 去调度?因为 M5 默认路径已经在这么干了,而 streaming 是比它更激进的一档:mmap 的页一旦被读就会留在 cache 里,除非显式 uncached。但这套访问模式的生命周期是「按 checkpoint 顺序读一遍、用完即弃」(这里所有权重的模式都一样),完全在作者的掌握之中,显式的 pread 加 uncached 比让内核猜要可控得多。为什么不选更重的异步 IO 方案?因为 macOS 上没有 io_uring,而一个 pthread 后台线程加双缓冲已经能把 SSD 喂饱(13–14.6 GiB/s 就是证据)。这些选择 README 里都给了答案,每个答案都是「在这块硬件上、为这个访问模式、选最土但最可控的那条路」。
这个 trade-off 有两个细节值得抄走。第一,输出 byte-identical——流式落盘只改调度不改数值路径,慢换来的每一字节内存都是干净的,画质不为内存买单。这正是前面说的「引擎内部收紧到字节级」标准的用武之地:没有这条标准,你无法向用户证明 streaming 是免费的(画质意义上),只能含糊地说「看起来差不多」。第二,两个分辨率给了两组减速比——512 正方形下 1.35 秒对 2.49 秒(慢 84%),864×480 下 2.14 秒对 2.68 秒(慢 26%)——等于明示你:这笔买卖划不划算取决于你的工作点。小分辨率下 SSD 带宽是瓶颈,大分辨率下计算占比上升、IO 占比下降,trade-off 自动变好。作者把两个点都测给你看,结论让你自己下,这比一句「性能影响可接受」高到不知道哪里去了。还有两个不能忘的注脚:streaming 与 --use-int8-row-fc2 不能共存(README 直接禁止),且交互会话里可以用 !ssd-streaming on 中途切换。
内存预算在文档里也被拆得很细:DiT 权重驻留不等于系统 RAM 占用(统一内存下还有 GPU 侧的驻留);prompt 编码和两个 VAE 分阶段运行、互不驻留,峰值为各自阶段的最大值而非总和(README 的原话:33B transformer、Qwen 编码器和解码器从不同时共存在统一内存里);--show 预览要额外加约 10 GiB 的 preview VAE,开之前先掂量。这种「memory budget 拆成表格写」的习惯,在推理项目里比想象中稀有——大多数项目的内存说明止步于「建议 16GB 以上」。
交互会话:Iris 风格的 ! 命令
h3-metal 有三种用法:--info 看模型布局和权重清单(不映射权重),单次 -p/--prompt 渲染一条片子,以及一个交互式会话。会话里是一组 Iris 风格的 ! 命令。!status 看当前会话状态、!seed 换随机种子、!seconds 改时长、!show 预览、!save 保存、!cache 管缓存、!help 查命令;!first/!last 钉首尾帧、!ref-image/!refs/!ref-remove 管理参考图像输入;!again 重复上一条 prompt、!open 控制出片后自动打开、!output 改输出目录。!ssd-streaming 和 !int8-row-fc2 两个大开关可以在会话中途切换。整组命令的哲学和 Redis-cli 一致:会话里能改的,绝不留到下一次进程启动。
这个设计的张力在于:视频生成是秒级到分钟级任务,调参的人却要试几十轮——改一帧、换个种子、调半档 steps。单次模式每轮都要重载权重、重编码 prompt,固定成本摊不掉。交互模式把这些成本一次性付清——会话里始终留着精确的 BF16 prompt 条件、准备好的 DiT 和视频解码器,让你在一个热会话里连续改条件,像调 synthesizer 一样调视频模型。甚至 streaming 模式下首块会在末块执行期间被预取,热会话的下一轮直接命中。
!first/!last 和 Ref2VA(ordered reference to video/audio,有序的图像/视频/音频参考输入机制)之间有明确的边界:两套条件机制互相独立、不可混用——首尾帧是「钉住时间轴的两端」,Ref2VA 是「给模型看一组有序素材」,语义不同,混用没有定义好的行为,所以直接禁止。参考文件用 !refs 查顺序、!ref-remove N 删一条、!refs clear 全清;首尾帧则用 !first clear/!last clear 解除。而且参考文件的文件名无意义——模型看到的是 <Picture 1>、<Picture 2> 这样的顺序占位符,不是 sunset_beach.jpg。这类「文件名是给人看的,顺序才是给模型的」说明,写文档的人显然被这个坑咬过,而且咬得不轻。
测试分层:四条不同的验收线
tests/ 目录有 21 个测试文件,按验收口径分成四层。test_real_* 加载真实权重验证各阶段(DiT 前向、两条 VAE、prompt 编码、参考条件),验「真的模型产出真的输出」——慢,但它是其余一切测试存在的前提。test_semantic_* 验语义等价,和前面那条「内容和运动必须一致」的跨引擎验收线呼应,容忍数值漂移,盯住内容漂移。test_metal(配合 make parity 和 misc/fixtures/ 里的 MLX 固定素材)验 GPU parity,即 Metal 路径和 host/MLX 参考在数值上的对齐程度,是 byte-identical 标准的执行者。bench_dit 是 DiT 性能基准,让「快了多少、慢了多少」这类话有可复现的来源,而不是某台机器上的一次手感。
四层各管一段,没有一层试图包办。真实权重测试太慢不能常跑,语义测试模糊不能定数值回归,parity 测试严苛但只盯 GPU 路径,bench 只管速度不管对错——分清谁负责哪条线,出了错才知道该信谁、该修谁。这个测试结构基本就是 vertical slice 工作流的质检镜像:每切一片,四层各过一遍,过了才算切完。
写给推理工程师的几句话
把 h3-metal 合上看,能带走的东西和 MiniMax-H3 这个模型本身关系不大。
最值钱的是「从头写」的成本重估。24000 行原生代码,一个作者,跑通了文本到视频、文本到音频、首尾帧插值、有序多模态参考的完整链路。在框架已经把「接一个新模型」压缩成「写几百行适配代码」的今天,这条路看起来笨;但笨的回报是每一行都懂——SSIM 在冲浪场景掉到 0.828 时,他能直接定位到 FC2 按行量化对高频运动的敏感性,而不是在框架的 issue 区排队等别人认领。极简数据结构、命令式 API、可读性优于抽象性——Redis 的三件套在这里原样复刻,只是战场从内存数据库换成了 GPU 推理。控制力本身是一种性能。
其次是把妥协做成参数,而不是藏进常量。steps/reuse/layers/core-reuse 四个旋钮、每个旋钮配 SSIM 数字、配「别开」的场景说明、配互斥规则——这比「我们的引擎又快又好」诚实得多,也可操作得多。用户拿到的不是一坨默认配置,而是一张标好坐标的地图。
再次是验收标准分层。跨引擎对语义,引擎内对字节,性能对 bench,端到端对真实权重。全文找不到一句「基本对齐」「大体一致」,每条线都有名有姓有数字。
还有文档的写法。README 里「为什么这么做」的篇幅和「怎么做」一样长:每个 preset 有验证数字和失效边界,每个约束(5+17*n 帧、32 的倍数、768×1344 上限、24 fps)都标了出身,每个开关都写着什么时候它会咬你。这种文档读起来不轻松,但它把「踩坑」前置成了「阅读」——而阅读永远比踩坑便宜。
最后是单人项目的生存法则。切片开发、四层测试、字节级对齐标准、模块化行数预算,这些选择单看都是风格,合起来是一套让一个人能长期驾驭一个 GPU 推理引擎的纪律体系。它不依赖英雄主义,依赖的是每一天结束时系统仍然可验证、可回滚、可通读。
动手练习与下一步
想自己动手,门槛不高。前置条件:一台 Apple Silicon 机器(M3 起),FFmpeg 和 FFprobe 在 PATH 里,以及从 Hugging Face 下载的 MiniMax-H3 模型快照。下面的示例沿用 README 的 fox prompt 作为基准 prompt:
git clone https://github.com/antirez/h3.c.git && cd h3.c
make -j8
./h3 --info -d ./MiniMax-H3
./h3 -d ./MiniMax-H3 --width 512 --height 512 --steps 6第一条命令看模型布局,确认权重就位;第二条用激进档出第一条片子,感受一下 4-7 步去噪的速度。练手可以从三条路线往下走。一是把 --steps 从 4 一路加到 50,配合 --show 观察每一步的细节增量,理解为什么 README 说「4 到 7 步之间每加一步都改善细节和运动」。二是用 --profile 对比 --layers 50 与 --layers 40 的 phase 耗时,把「层瘦身省了什么」落到数字上。三是开 --token-reduction 和 --ssd-streaming 各跑一遍同一 prompt,对照 SSIM 表验证本文说的互斥和 trade-off。
写 prompt 也有一层。H3 的发布工作流期待 Context-IR 式的结构化描述,而非一句话:「场景是什么」(Scene)、「发生什么动作」(Action)、「机位怎么摆」(Camera)、「光线风格」(Look)、「要什么声音」(Audio)拆开交代,反而比堆砌形容词更省你真调参的回合数。身份和物体数量关键时写死——两只鸟就是「two birds」,一个 rider 一块板就明说,模型不会替你脑补。种子默认是 42,--seed N 接管随机流;要在不同分档之间公平对比,务必保证 prompt、seed、分辨率、帧数、步数五者一致,只动你要测的那一个旋钮。若 FFmpeg 不在手边,-o '' 可关掉 MP4 编码,配合 --frames-dir 把每帧回调写成 PPM,回头再慢慢合成。
几个常见的错误组合提前排掉:--reuse 和 --core-reuse 同时开会被参数校验拒绝;--token-reduction 叠上 --layers 40 --reuse 3 会出振铃和鬼影;--ssd-streaming 和 --use-int8-row-fc2 不能共存;--seconds 和 --frames 互斥。真踩了这些,README 里的校验和数字就是你的排查地图。
下一步如果想深入,可以读 h3_dit.c 里 stream slots 的双缓冲实现,或者 h3_gpu.m 里 mmap 零拷贝与 F_NOCACHE 读取的分工——那是这篇仓库笔记里最浓缩的两段。之后的旋钮怎么拧、哪个组合不能碰、哪个开关适合你的素材类型,README 里的 SSIM 表比任何第三方 benchmark 文章都诚实。
Redis 当年教会业界的是「一个数据结构服务可以做到多简」。h3-metal 现在问的是另一个问题:当模型固定、硬件固定,一个推理引擎最少需要多少行代码、最多能交出多少控制权。目前的答案是 24000 行原生代码、41 个旋钮——以及一个仍然在一行行往下写的仓库。这个答案未必适合所有人,但它证明了一件事:在框架林立的时代,一个人、一门老语言、一块芯片,依然能把一个视频生成模型按进自己的手心。
参考
- antirez/h3.c 仓库:本文所有数字(行数、参数档位、SSIM、耗时、内存)的核对来源,核对时间为 2026-08-18,仓库 commit(提交)
8974cc0。 - h3.c README:Tutorial、性能数据和设计取舍的完整原文。
- MiniMax H3 官方发布博客:模型定位(全模态、2K、15 秒、原生立体声)与架构说明。
- MiniMax-AI/MiniMax-H3:官方代码与权重仓库(H3-Context-IR / H3-Base / H3-Regenerate-2K 三模块)。
参与讨论
使用 GitHub 登录。欢迎补充事实、异议与实践。
讨论暂时无法加载。