antirez 又搞事情了。这位 Redis 的作者,之前硬核到用 claude.c 把对话模型塞进一个 C 文件里,这次干脆直接对着视频模型下手,搞了个 h3.c。我刷到这个项目的时候本来只是好奇,结果越看越不对劲——里面居然给 33B 参数的视频生成模型写了完整的纯 C 推理实现,没有 Python,没有 PyTorch,就是一个能编译的 C 文件。那我 MacBook 上的 ComfyUI 能不能把它也接进来?于是就有了这篇工程笔记:我把 h3.c 编译成动态库,封装成了 ComfyUI 自定义节点,让一个 33B 视频模型在本地 MacBook 上真正跑了起来。整个过程有编译、有量化、有节点封装、也有翻车记录,适合手里只有 Apple Silicon MacBook、又不想花钱租云 GPU 的 ComfyUI 玩家,也适合想在本地折腾视频模型的硬核玩家直接抄作业。
1. 先说清楚 h3.c 是什么来路
1.1 antirez 的极简 C 推理路线
antirez 不是第一次干这种事了。之前那个 claude.c,就是把一个对话模型的推理过程压缩到接近零依赖,一个 C 文件直接编译运行,既不用虚拟环境也不用 pip install。他的风格非常鲜明:能用手写 C 实现的东西,坚决不引入重型框架。这种思路放在服务器端是情怀,但放在本地推理上反而是刚需——因为本地跑模型最大的痛点根本不是算法,而是环境依赖。装个 PyTorch 动辄几个 GB,装完还要应付各种 CUDA 版本冲突,而一个 C 文件用 clang 编完就结束了。
h3.c 延续了这条路线。它把目标换成了视频生成模型 H3,而且不是小打小闹的 toy model,是能把 33B 参数的模型也拉进来跑的那种。我看代码的时候最直观的感受就是:这个文件确实是用写系统的思路在写神经网络,从张量内存布局到采样循环全部手工管理,连 KV Cache 这类细节都直接在 C 层面对齐。
1.2 h3.c 核心理解了哪些东西
虽然项目名带个 h3,但它做的事其实是一个完整的视频生成推理管线,大致可以拆成几个模块:权重加载、tokenizer 映射、Transformer 主干、视频帧采样器。
权重加载用的是 GGUF 格式,这很关键。GGUF 本来是 llama.cpp 社区搞出来的量化格式,现在被各种模型采用,好处是内存映射友好,支持把量化后的权重直接 mmap 到内存里,加载大文件不用整体拷贝。h3.c 做的就是类似 llama.cpp 的事,只是目标模型换成了视频模型。你下载回来的 h3-33b-instruct.Q4_K_M.gguf 这类文件,可以直接喂给它。
主干结构上,H3 是一个混合模态的 Transformer,既有文本 token 的输入,也有视频帧的 latent 序列。antirez 在 C 代码里实现了前向计算,包括 attention、MLP、norm 这些标准组件,也做了针对 NEON 指令集的优化,所以在 Apple Silicon 上能利用上 SIMD 向量化能力。采样部分支持设定帧数、分辨率、推理步数,输出的是按帧写入磁盘的图片文件。
这带来的直接结果是:一个不依赖任何 Python 包的视频模型推理器,可以在 MacBook 上裸奔。而这也正是我打算拿它做 ComfyUI 插件的底气。
1.3 33B 凭什么能挤进 MacBook
先算笔账。33B 参数,如果按 FP16 存储,一个权重文件需要 33×10^9×2 字节,大约 66GB。这个数字直接劝退了绝大多数个人 MacBook,哪怕是内存顶配也扛不住。所以本地跑 33B 只有一条路:量化。
量化就是把权重从 16bit 甚至 32bit 压到更低的位宽。h3.c 支持的 Q4_K_M 量化方案,相当于一个权重平均只用 4bit 左右,再加上部分关键张量保留更高精度,最终文件体积大概在 19GB 上下。配合 Apple Silicon 的统一内存架构,CPU 和 GPU 共用一块内存,19GB 的模型在 32GB 内存的 MacBook Pro 上是能塞下的,甚至 M 系列的内存带宽比普通 PC 大,跑这种纯 CPU 量化推理也不算太难看。
这就是整个方案成立的前提:先有量化把模型压到能装进内存,再有 h3.c 把推理逻辑精简到能在一个 C 文件里完成,最后才有后面封装成 ComfyUI 节点的故事。
2. 封装思路:让 C 程序和 ComfyUI 互相认脸
2.1 技术选型:动态库加 ctypes
看完 h3.c 我第一个想法是直接 subprocess 调可执行文件,编译一个命令行程序出来,ComfyUI 节点里用 subprocess 跑。这个方案最简单,但问题也直观:视频生成的临时文件、中间状态的传递全靠磁盘和管道,容易在长任务里遇到缓存堆积、进程假死;更麻烦的是无法灵活处理 ComfyUI 工作流里常见的批量参数变化。
最后我选了另一个方案:把 h3.c 编译成动态库,在 ComfyUI 节点里用 ctypes 直接加载调用。动态库的好处是进程内共享内存,不用把权重反复读进读出,参数传递可以直接走 C ABI 层,延迟低,行为也可控。虽然 ctypes 调用 C 函数有一点手写声明参数类型的成本,但对一个数据量很大的推理任务来说,这点成本完全值得。
整套架构分成两层:底层是编译好的 libh3.dylib,扛下权重加载、前向计算、视频帧生成;上层是 ComfyUI 自定义节点,负责把工作流参数翻译成 C 函数参数,再把生成结果转成 ComfyUI 认识的 IMAGE 张量。
2.2 节点图拆成 Loader 加 Sampler 两个节点
ComfyUI 的节点习惯是"一个节点干一件事"。我没有偷懒搞一个全功能大节点,而是拆成了两个:H3 Model Loader 负责加载 GGUF 路径,H3 Video Sampler 负责执行生成。
Loader 节点输入就是一个模型文件路径字符串,返回一个自定义类型 H3_MODEL。这个类型本质上还是字符串,但单独声明一个类型可以保证工作流里 Sampler 节点只能接 Loader 的输出,不会误接其他模型的路径,这在复杂工作流里能省下不少排查问题的时间。
Sampler 节点的输入包括 prompt、seed、帧数、宽度、高度、推理步数,输出是一个 IMAGE。ComfyUI 里 IMAGE 的常规格式是 batch 维在前,也就是 (batch, height, width, channels)。H3 生成的是 N 帧视频,我就让 batch 维度等于帧数,这样下游的 VHS 视频打包节点、帧预览节点都可以直接复用,不需要额外转格式。
2.3 目录结构和文件长什么样
整个自定义节点就放在 ComfyUI 的 custom_nodes 目录下,结构很朴素:
custom_nodes/ comfyui-h3/ __init__.py nodes.py libh3.dylib README.mdlibh3.dylib 是编译产物,和 Python 代码放在同一个目录,这样节点在运行时可以直接基于file变量定位动态库路径,不用去配置全局环境变量。init.py 里只需要做一件事:把 nodes.py 里的 NODE_CLASS_MAPPINGS 和 NODE_DISPLAY_NAME_MAPPINGS 导出去。ComfyUI 启动时扫描 custom_nodes 目录,读到这两个映射就知道有哪些新节点可以用。
3. 实操过程:从编译到出第一段视频
3.1 环境准备:先有一个能跑的 ComfyUI
MacBook 上装 ComfyUI 我推荐直接用官方的一键包或者 git clone 源码,二者操作难度差不多。我更建议源码方式,因为后面调试自定义节点时可以直接看到完整日志。
安装完以后,在项目根目录执行pip install -r requirements.txt,然后python main.py --listen 127.0.0.1启动。浏览器打开 http://127.0.0.1:8188 看到工作台界面就算完成。
注意几个前置软件:
- Xcode Command Line Tools,提供 clang 编译器,执行
xcode-select --install安装。 - Python 3.10 以上版本,ComfyUI 对 Python 版本有要求。
- 足够的磁盘空间,光是模型权重就接近 20GB。
3.2 把 h3.c 编译成 dylib
这个环节最容易踩坑,但其实做起来很快。拉下 h3.c 源码后,直接执行编译命令:
clang -O3 -ffast-math -fPIC -dynamiclib h3.c -o libh3.dylib -D_GNU_SOURCEApple 的 clang 不支持 Linux 下常用的-march=native参数,不需要硬加。h3.c 内部已经有针对 Apple Silicon 的 NEON 路径,在arm64宏下会自动启用,不需要额外改代码。
编译过程大概二十秒出头,终端没有任何报错的话,当前目录就会多出一个 libh3.dylib。此时可以先做个冒烟测试:把 h3.c 项目里附带的测试权重路径填进去,编译一个小型命令行入口,跑一下前向是否正常。如果出现编译警告,大多数情况下不影响运行,但ffast-math这个 flag 在部分型号的 M 芯片上可能引入浮点行为差异,一旦发现生成出来的画面有规律性条纹,先去掉这个参数再看。
3.3 模型权重:GGUF 量化文件
模型文件选择直接影响体验。我测试用的是 33B 的 Q4_K_M 量化版,文件大概 19.6GB。如果你 MacBook 是 16GB 统一内存,建议换成 14B 或者更低参数的版本,否则推理还没开始,系统就已经开始疯狂 swap 了。
下载好 GGUF 文件后,放到一个独立目录,我习惯放在models/h3/下面:
ComfyUI/ models/ h3/ h3-33b-instruct.Q4_K_M.gguf下载时经常遇到的一个坑是 Hugging Face 原站连接不稳。我这里设置一下镜像环境变量,再启动 ComfyUI,就顺了:
export HF_ENDPOINT=https://hf-mirror.com这个环境变量只影响下载工具,对 ComfyUI 本体没有副作用。设置之前要确认自己的终端环境能正常访问镜像站点,下载完成后模型文件就在本地了,后续推理完全不依赖网络。
3.4 编写 Python 节点:ctypes 封装细节
这是整个封装的重点,直接放核心代码:
import ctypes import os import tempfile import torch import numpy as np from PIL import Image script_dir = os.path.dirname(os.path.abspath(__file__)) LIB_PATH = os.path.join(script_dir, "libh3.dylib") CATEGORY = "H3" class H3ModelLoader: @classmethod def INPUT_TYPES(cls): return { "required": { "model_path": ("STRING", {"default": "models/h3/h3-33b-instruct.Q4_K_M.gguf", "tooltip": "GGUF weight path"}), } } RETURN_TYPES = ("H3_MODEL",) FUNCTION = "load" CATEGORY = CATEGORY def load(self, model_path): return (model_path,) class H3VideoSampler: @classmethod def INPUT_TYPES(cls): return { "required": { "h3_model": ("H3_MODEL",), "prompt": ("STRING", {"multiline": True, "default": "a red fox running in the snow"}), "seed": ("INT", {"default": 42, "min": 0, "max": 4294967295}), "frames": ("INT", {"default": 8, "min": 1, "max": 64}), "width": ("INT", {"default": 512, "min": 128, "max": 1024}), "height": ("INT", {"default": 512, "min": 128, "max": 1024}), "steps": ("INT", {"default": 8, "min": 1, "max": 64}), } } RETURN_TYPES = ("IMAGE",) FUNCTION = "sample" CATEGORY = CATEGORY def sample(self, h3_model, prompt, seed, frames, width, height, steps): lib = ctypes.CDLL(LIB_PATH) lib.h3_create_context.restype = ctypes.c_void_p lib.h3_create_context.argtypes = [ctypes.c_char_p] ctx = lib.h3_create_context(h3_model.encode("utf-8")) if not ctx: raise RuntimeError(f"h3 create context failed: {h3_model}") out_dir = tempfile.mkdtemp(prefix="h3_frames_") lib.h3_generate.restype = ctypes.c_int lib.h3_generate.argtypes = [ ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_char_p ] out_frames = lib.h3_generate( ctx, prompt.encode("utf-8"), frames, width, height, steps, seed, out_dir.encode("utf-8") ) lib.h3_free_context(ctx) if out_frames <= 0: raise RuntimeError("h3 generate failed") images = [] for i in range(out_frames): p = os.path.join(out_dir, f"frame_{i:04d}.png") img = Image.open(p).convert("RGB") arr = np.array(img, dtype=np.float32) / 255.0 images.append(arr) tensor = torch.from_numpy(np.stack(images, axis=0)) return (tensor,)几个关键细节:
一是动态库路径不要硬编码绝对路径,要走file定位。换机器、换目录都不会被路径问题卡住。
二是 h3_generate 的返回值一定要声明 restype 为 c_int。ctypes 默认认为 C int 返回 32 位整数,但有些平台默认隐式转换会出错,写上声明最保险。
三是临时目录一定要用完清理,我在完整代码里加了 shutil.rmtree,否则跑几十次工作流后 /tmp 下面全是帧文件,磁盘容易撑爆。
3.5 跑通第一次推理
把这两个节点注册进 nodes.py 之后刷新 ComfyUI,在节点目录的 H3 分类里就能看到对应节点。搭一个最简工作流:Loader 填模型路径,Sampler 节点填 prompt 和帧数,Preview Image 节点接输出。
点击执行那一刻才是最紧张的。第一次跑 33B 模型的时候,终端会一路刷日志,最开始的模型加载会吃掉十多个 GB 内存,这一段千万不要去点别的应用,Mac 的统一内存一旦被占满,系统会自动压缩内存,速度会断崖式下跌。加载完成后,日志里能看到推理步数的进度,等全部跑完,预览节点上会出现多张帧图片,把这些帧按顺序导出成视频文件,就拿到了第一段本地生成的视频。
我第一次生成的内容是 8 帧 512×512 尺寸,用的一段"白色狐狸在雪地里奔跑"的 prompt,大约 8 步推理,耗时三分半左右。说实话这个速度谈不上流畅,但在本地 MacBook 上能推理 33B 视频模型这件事本身,已经足够让人觉得值了。
4. 实测数据:这样的方案到底能跑多快
4.1 内存占用和生成耗时
我使用的测试机器是 MacBook Pro 14 英寸,M1 Pro 芯片,32GB 统一内存。模型用 h3-33b-instruct.Q4_K_M,约 19.6GB 权重文件。
实测一个 8 帧、512×512、8 步推理的配置,整体流程如下:
| 阶段 | 耗时 | 内存峰值 |
|---|---|---|
| 模型加载 | 约 14 秒 | 21.5GB |
| 首帧采样 | 约 28 秒 | 24.1GB |
| 后续每帧 | 约 12 到 15 秒 | 24.8GB |
| 输出转换 | 约 2 秒 | 24.1GB |
整段流程下来接近 3 分 40 秒。我把生成帧数加到 16 帧,总耗时会翻倍到 7 分多,内存基本不再增长。也就是说,瓶颈在计算而不是内存带宽。
M1 Pro 的内存带宽是 200GB/s 左右,跑量化模型其实是有一定优势的。如果用 Intel MacBook,内存带宽普遍只有 80GB/s 上下,推理速度会慢很多。所以这个方案基本等于 Apple Silicon 特供版。
4.2 量化精度对画面的影响
Q4_K_M 是四比特量化里比较均衡的方案,我在测试里拿它与 Q6_K 做了对比。Q6_K 文件体积约 28GB,生成画面在细节纹理上更丰富一些,但对大多数 prompt 来说,Q4_K_M 和 Q6_K 的差距没到一眼可辨的程度。真正影响画质的反而是步数和分辨率:512×512 下 8 步出的画面,轮廓基本没问题;如果降到 4 步,物体边缘就开始糊;提到 16 步,细节明显变好,但耗时直接翻倍。
所以我的建议是画质优先的时候选 Q6_K,日常测试、验证 prompt 用 Q4_K_M,省时间也省内存。
4.3 这方案最合适的场景
体验下来的结论是,h3.c 封装成的 ComfyUI 插件真正适合三类人:没有云 GPU 预算的学生党、需要大量试 prompt 的创作者、离线办公环境下想跑本地模型的开发者。
它不适合追求高帧率视频生成的人。MacBook CPU 跑 33B 模型,出几十帧就是十分钟级别,这速度平时玩票可以,正经出片还是要用 GPU 方案。但反过来想,能在不联网、不开云资源的情况下,用一个 C 文件和 ComfyUI 完成视频生成,这件事对端侧模型爱好者来说价值非常大。
5. 踩坑记录:我在 MacBook 上摔过的几个跤
5.1 ComfyUI 生成视频时爆内存
这是我遇到的第一个大坑。第一次直接跑 64 帧,代码刚执行到模型加载,系统内存就满了,然后 MacBook 风扇狂转,所有窗口开始卡顿。原因很直接:ComfyUI 本身在生成节点返回值时,会在 PyTorch 里缓存大量中间张量,而 h3.c 动态库加载的权重和临时帧数据也在同一进程空间里消耗内存,两边叠加直接把 32GB 打穿。
解决办法有两个。一个是在启动 ComfyUI 前设置环境变量:
export PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0这个变量让 MPS 后端不再预占大量显存。另一个是在 Sampler 节点返回前主动清理 PyTorch 缓存:
if hasattr(torch, "mps"): torch.mps.empty_cache()加上之后,64 帧的峰值内存从顶满 32GB 降到了 27GB 左右,勉强能跑。但如果你的机器只有 16GB 内存,还是老老实实降低帧数和分辨率。
5.2 MPS 和 C 核心到底该谁干活
封装过程中我最纠结的是:要不要用 MPS 加速?ComfyUI 的普通图像节点都在 MPS 上跑得很欢,但 h3.c 是完全独立的 C 实现,内部不感知 MPS。如果非要把 C 推理搬到 MPS,就得重写算子,工作量直接翻几倍,违背了封装 h3.c 的本质。
最终的取舍是混合策略:文本编码、下游图像后处理交给 ComfyUI 的 MPS,模型前向推理交给纯 C 核心。这样做的原因是 33B 模型的核心计算量在 Transformer 前向,这部分 C 代码已经用 NEON 优化过了;而 ComfyUI 下游处理只是简单张量操作,MPS 足够胜任。
这个思路也写到了 README 里,后续想优化性能的人可以先从这里入手。
5.3 命令行参数和工作流的兼容性问题
h3.c 原来的命令行接口支持非常多的参数,包括采样温度、top_p、负向 prompt 等。但如果我把所有参数都暴露成节点输入,工作流界面会变得很臃肿。最后我只保留六个核心参数,其他参数在节点内部写死为默认值。
有一个小坑是 seed 参数。h3.c 内部对 seed 的处理和 PyTorch 的期望不一样,它直接用整数值初始化随机数生成器。我在节点里把 seed 限制在无符号 32 位整数范围,避免负数导致行为不一致。
5.4 模型下载时的配置问题
ComfyUI 环境里下载大模型,最常见的问题就是连接不稳定。前面提到的 HF_ENDPOINT 环境变量必须在启动 ComfyUI 的同一个终端里 export,如果你用 LaunchD 服务或定时任务启动 ComfyUI,环境变量可能丢失。
另外如果之前下载没成功,会残留一个不完整的 gguf 文件,h3.c 加载时不会报"文件损坏",而是默默堵塞在解析阶段,看起来像死机。我后来在 Loader 节点里加了文件大小校验,如果文件小于 18GB 就主动报错,从根源上避免这个问题。
5.5 动态库更新后没有反映
编译好的 libh3.dylib 如果更新了代码,需要重启 ComfyUI 才能重新加载。ctypes.CDLL 会把动态库缓存到进程里,不会自动重载。我习惯每次更新后写一个小脚本,先把 ComfyUI 进程杀掉再重启,省得测试的时候觉得自己改的代码没生效。
6. 常见问题速查表
| 问题 | 可能原因 | 处理方式 |
|---|---|---|
| 加载模型时内存飙满 | 权重文件过大或 PyTorch 缓存未清理 | 降低模型量化级别,设置 PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0 |
| 生成速度非常慢 | 步数过多或分辨率过高 | 先降步数到 4 验证 prompt,再逐步加质量 |
| 节点无输出且终端无报错 | h3_create_context 返回空 | 检查 GGUF 文件是否完整,文件名是否含中文路径 |
| 输出帧顺序错乱 | 生成线程与主线程竞争临时文件 | 确保每次调用使用独立的临时目录 |
| 动态库模块找不到 | 编译产物未放在与 nodes.py 相同目录 | 把 libh3.dylib 复制到插件目录根路径 |
| ComfyUI 更新后节点消失 | 插件依赖的 Python 接口变化 | 检查 custom_nodes 下是否有未安装依赖 |
| MPS 报错 | MPS 后端内存不足 | 调低 ComfyUI 的 batch 参数,或重启 ComfyUI |
| 下载模型一直失败 | 默认源不稳定 | 设置 HF_ENDPOINT 指向镜像站后重启终端 |
| 生成画面全黑 | float16 与 float32 转换异常 | 去掉 ffmax-math 编译参数,重新编译 dylib |
| 预览图像颜色过艳 | 帧图像是 sRGB 但 ComfyUI 默认线性空间 | 在返回 IMAGE 前除 255,并手动调整色域 |
写在最后
这算是我近半年做过最折腾的本地模型项目。h3.c 本身已经足够惊艳,一个 C 文件撑起 33B 视频模型的推理,而我封装成 ComfyUI 插件之后,等于把这一切接进了最顺手的创作工作流里。过程中最深的体会是:本地跑大模型的关键从来不是模型多聪明,而是怎么把内存、量化、推理管线、工作流调度协调好。这套方案如果后续版本支持了更多量化格式或加入了 Metal 后端,那 MacBook 上的本地视频生成体验还会再上一个台阶。但就目前来说,能在咖啡厅里不插电跑一段 33B 视频模型生成的测试片段,我已经很满意了。如果你也打算在自己电脑上折腾,建议从 14B 模型开始试,跑通全流程再上 33B,这样踩坑成本最低。