Apple Silicon 本地跑 33B 视频生成模型:ComfyUI 插件实操
这个项目起因其实特别简单我想在一台 M3 Max 的 16 英寸 MacBook 上把社区里那个 33B 的开源视频生成模型本地跑起来并且让它能作为自定义节点出现在 ComfyUI 工作流里。断断续续折腾了两周最后把 antirez 近期开源的 h3.c 单文件 C 推理实现封装成了一个 ComfyUI 插件24 帧 640×384 的短视频在本机跑大概半小时出头过程中电脑还能正常回消息、浏览网页。这个标题听起来像整活但每一步都是正经工程权重量化、ctypes 桥接、内存带宽调优、采样器参数缺一个都跑不出片。这篇笔记适合两类人一类是想在 Apple Silicon 上本地跑视频模型、手里没有大显存 GPU 的 ComfyUI 玩家另一类是好奇“单文件 C 推理引擎怎么塞进 Python 节点系统”的工程党。我会把动机、架构、完整实操、踩坑记录和问题速查表都写出来尽量做到你可以照着复现。1. 为什么要在 MacBook 上跑 33B 视频模型1.1 先厘清一个坑h3.c 不是那个 H3先说一个很容易被名字带偏的点如果你直接搜 h3.c大概率会先看到 antirez 早年写的 H3 六边形网格实现h3-polyfill 那个项目那是做地理空间索引的跟视频模型一点关系都没有。这次说的 h3.c是 antirez 在 2025 年延续 gpt.c 思路做的单文件 C 推理引擎目标是让大规模 transformer 在没有 CUDA 的普通 CPU 上也能跑并且对 Apple Silicon 的 NEON / AMX 做了针对性优化。我在动手前特意把仓库里的代码结构过了一遍没有外部依赖整个前向逻辑集中在一个文件里编译产物是一个动态库调用接口极其精简。如果你 fork 错了仓库编译出来的是一堆 geohash 函数那后面就全白折腾了——这个坑我见过不止一个人踩包括我自己一开始也差点走错。1.2 选型逻辑为什么不用 llama.cpp看到这里你肯定会问Mac 上跑大模型社区早就有 llama.cpp 和 MLX 了为什么还要用一个名不见经传的 h3.c我的理由有三个llama.cpp 是“通用运行时”不是“开发者套件”。它的重点是文本模型和 GGUF 格式采样 API 也是围绕 LLM 设计的。视频 DiT 这种场景需要把 text encoder、去噪循环、VAE 捏在一起中间还有大量自定义调度逻辑塞进 llama.cpp 反而别扭。h3.c 代码量小前向逻辑一目了然。我可以在 C 侧直接改采样循环、配置 CFG 双路并行、控制每一层的内存分配。llama.cpp 那套 batched inference API 对我来说是黑盒改起来成本高。编译目标单一。我只跑 Apple Silicon不需要维护 x86、CUDA、ROCm 那套构建矩阵。h3.c 编译完就是一个 .dylibComfyUI 的 Python 侧用 ctypes 直接调链路短、依赖少。当然也得说实话h3.c 的生态和算子覆盖面远不如 llama.cpp。如果不是为了“研究加定制”直接等官方 ComfyUI 内核支持这个模型或者用 llama.cpp 的 MPS 后端会更省事。选 h3.c 本质上是一次“技术改造”的取舍不是因为它最成熟。1.3 投入产出比什么时候值得折腾本地跑 33B 视频模型的真实价值不在“省一张显卡的钱”而在三个场景素材敏感。我手上有些测试素材不能出本机云 GPU 方案直接排除。批量试参数。做采样器和量化实验时一个组合可能要反复跑几十次云上排队加传输的时间比本地慢得多。离线可用。出差没网的环境下本地跑通意味着随时能出片。反过来如果你要的是快速出片、长视频、1080p 高帧率那别折腾了云 API 或者一张 4090 才是正解。我的判断是64GB 统一内存的 Apple Silicon 是本地跑这个量级模型的临界点低配机器投入产出比非常差。2. 插件整体设计ComfyUI 节点和 C 引擎怎么捏在一起2.1 节点划分与数据流ComfyUI 自定义插件的结构本身不复杂难的是“职责切分”。我最终把链路拆成了五个节点H3CheckpointLoader加载权重元信息。注意这里并不真正把权重读进内存而是记录文件路径和量化格式留给 C 引擎做 mmap 映射。H3TextEncode走 torch 的 MPS用模型的 text encoder 把正负提示词编码成 embedding。H3Sampler核心去噪节点。把文本 embedding 和初始噪声传给 C 引擎执行 CFG 双路采样一帧一帧地出 latent。H3VAEDecode把采样完的 latent 解码成视频帧。VAE 对精度敏感我用 fp16 跑 MPS。H3SaveVideo把帧批次用 ffmpeg 拼成 mp4。这样划分的好处是每个节点都能独立测试、独立替换。比如我可以不用 h3 的 VAE而是换成官方 ComfyUI 的 VAE 节点这在工作流调试时非常救命。数据流上文本 embedding 是 float16 的 tensorlatent 在 C 引擎里也是 float16传递过程只搬运指针不做整块拷贝。2.2 桥接层ctypes 与 tensor 搬运这里我要多说几句因为这是整个插件里最容易翻车的地方。我选择 ctypes 而不是 pybind11原因是 ComfyUI 的 Python 版本经常跟着环境变pybind11 每次升级都要重编译维护成本太高。ctypes 的 CDLL 是纯 Python 层调用只要 C 侧导出的是标准 C 接口任何 Python 3.10 到 3.12 都能跑。一个关键技巧C 引擎和 torch 之间传 tensor不需要序列化直接拿内存地址。torch 张量只要保证是 contiguous 的用.data_ptr()就能把地址传给 C 侧。但这里有个前提——必须是 CPU 张量。MPS 张量的内存不在 CPU 可寻址空间必须先.cpu().contiguous()。import ctypes lib ctypes.CDLL(str(HERE / libh3.dylib)) # 关键不设 argtypes 的话ctypes 默认按 32 位 int 传参 # 指针会被截断C 侧拿到的地址直接段错误 lib.h3_create.restype ctypes.c_void_p lib.h3_create.argtypes [ctypes.c_char_p, ctypes.c_int] lib.h3_step.restype ctypes.c_int lib.h3_step.argtypes [ ctypes.c_void_p, # ctx ctypes.c_void_p, # cond embedding 地址 ctypes.c_void_p, # uncond embedding 地址 ctypes.c_float, # t ctypes.c_void_p, # latent 地址 ]我第一次跑的时候忘了给h3_create设置 argtypes结果句柄被截断成 32 位C 侧收到的根本不是一个有效指针整个下午都在排查 EXC_BAD_ACCESS。这个坑几乎每个用 ctypes 调 C 库的人都会踩务必把每一组参数的类型都写清楚。2.3 权重转换与量化策略h3.c 自己带一个权重格式核心是一份“层名映射表”。33B 视频模型的 checkpoint 是 safetensors 格式我需要写一个转换脚本按层名把权重逐层抽出来python convert.py \ --input model.safetensors \ --out model_h3_q4.bin \ --quant q4 \ --template diffuser转换脚本的逻辑不复杂读 safetensors 的 header按命名规则把diffusion_model.blocks.0.attn.q.weight这类名字映射到 h3.c 的内部张量名然后做分组量化写出去。量化格式类似 GGML 的 Q4_032 个权重一组每组带一个 fp16 scale。量化决策直接影响能不能跑起来我把不同精度的实测数据整理如下精度33B 权重体积建议机器内存主观画质相对速度fp16约 66GB128GB 以上参考基准1.0xQ8约 36GB96GB 以上接近原版约 1.2xQ6约 27GB64GB 以上良好约 1.3xQ4约 20GB64GB 刚好细节略有损失约 1.5x注意速度这一栏这类模型的瓶颈是内存带宽而不是计算权重体积越小单位时间读入的数据越少速度反而更快。Q4 在我机器上是最后的甜点——再往下量化画质劣化就开始影响可用性了。text encoder 和 VAE 我坚决保持 fp16这两部分对精度敏感而且体量小省不了多少内存没必要冒画质风险。3. 实操全记录从零到出片3.1 环境准备与编译先说我的环境macOS 15.xM3 Max 芯片64GB 统一内存外接 100W 电源。磁盘建议至少留 200GB 空闲因为原始 checkpoint 加上转换后的量化文件体积都不小。# 依赖 brew install ffmpeg python3.11 -m venv venv source venv/bin/activate pip install torch --index-url https://download.pytorch.org/whl/cpu git clone https://github.com/comfyanonymous/ComfyUI.git cd ComfyUI pip install -r requirements.txt # 编译 h3.c 动态库 git clone https://github.com/antirez/h3.c cd h3.c clang -O3 -stdc11 -mcpuapple-m3 -dynamiclib -o libh3.dylib h3.c -lpthread编译时的几个注意点-mcpuapple-m3是针对 M3 的调度优化换芯片要改成对应的 apple-m1 / apple-m2 / apple-m4。不要加-ffast-math。这个优化跑基本测试没问题但视频模型生成到后半程会出现 NaN导致整段输出花屏。后面细说。线程数不要想当然设满。h3.c 的多线程是手动控制的我实测 8 线程比 12 线程更快因为线程多了反而在内存带宽上互相争抢。模型文件下载这块我的建议是别用浏览器用支持断点续传的命令行工具慢慢拉文件很大断一次从头再来非常折磨。3.2 插件目录与节点实现插件目录结构如下custom_nodes/comfy-h3/ ├── __init__.py ├── h3_node.py ├── convert.py ├── libh3.dylib └── weights/ └── model_h3_q4.bin节点代码本身不复杂核心是把 C 引擎的调用封装成 ComfyUI 的标准节点类class H3VideoSampler: classmethod def INPUT_TYPES(cls): return {required: { ckpt: (H3CKPT,), text: (STRING, {multiline: True}), frames: (INT, {default: 24, min: 8, max: 64, step: 4}), width: (INT, {default: 640, min: 256, max: 1280, step: 16}), height: (INT, {default: 384, min: 256, max: 1280, step: 16}), steps: (INT, {default: 25, min: 10, max: 60}), cfg: (FLOAT, {default: 4.5, min: 1.0, max: 15.0}), seed: (INT, {default: 42}), }} RETURN_TYPES (H3LATENT,) FUNCTION sample CATEGORY h3/video def sample(self, ckpt, text, frames, width, height, steps, cfg, seed): ctx lib.h3_create(str(ckpt.path).encode(), 8) # ... 和 text encoder 联动拿 embedding 地址逐帧调用 h3_step return (latent,)__init__.py里把节点类注册进NODE_CLASS_MAPPINGS和NODE_DISPLAY_NAME_MAPPINGSComfyUI 重启后刷新节点列表就能搜到。流程上最容易出错的一步h3.c 的采样循环是“逐步去噪”而外部拿到的 embedding 是 torch tensor。在每步调用前必须确认 embedding 是 CPU、float16、contiguous 的。我在代码里写了个小工具函数做现地转换并且把地址打日志方便核对。3.3 一次完整生成的实际记录跑通的第一个成片我用的配置是提示词a shiba inu running in the rain, cinematic lighting, shallow depth of field24 帧640×384steps 25CFG 4.5seed 42权重 Q4线程 8实测数据M3 Max 64GB阶段耗时说明文本编码约 1 分钟MPS 上跑 text encoder去噪采样约 28 分钟C 引擎核心耗时部分VAE 解码约 2 分钟MPSfp16视频封装约 20 秒ffmpeg 转 h264总体约 32 分钟。采样 28 分钟里并不是均匀的前几步噪声大、信息量小反而快越到后面越慢因为模型逐渐“确定”画面内存访问模式变了。同样配置放在云上 A100 大概 3-5 分钟但考虑到上传素材、排队、下载结果本地跑并没有想象中那么亏。第一次跑通看到那只柴犬在雨里动起来的时候说实话挺有成就感的。但我也要诚实地说速度就是这个水平别指望视频生成流畅到“所见即所得”它更适合晚上睡觉前丢一个任务进去第二天早上收片。4. 性能调优与踩坑实录4.1 量化、精度与 NaN 的血泪史刚才提到-ffast-math的事这里展开说。开了这个编译器优化前 16 帧一切正常到第 17 帧 latent 开始出现 NaN整段视频从一帧漂亮画面瞬间变成彩色噪点。排查了很久最后是逐层 dump tensor 才发现是 fast-math 重排浮点运算导致特定层溢出。关掉之后问题消失。这个经历给了一个教训C 引擎的浮点行为跟 Python 侧 torch 的参考实现不完全一致。所以我在插件里加了一个“逐层校验模式”让 h3.c 每跑一层就把中间结果 dump 出来跟 torch 参考实现逐层比对允许小误差但不能有量级差异。这个模式平时关着出 bug 的时候才打开用来定位是转换脚本的问题、量化的问题还是 C 引擎前向的问题一次只允许一个误差来源。4.2 内存、分页与 macOS 的脾性Mac 的统一内存架构既是优势也是麻烦。说好听点是 CPU 和 GPU 共享内存说直白点就是“显存不够的时候你的系统内存也会被拖下水”。我用两个手段监控活动监视器的“内存压力”曲线而不是看某个进程的 RSS 数字。终端里memory_pressure命令配合vm_stat看 swap 使用量。权重文件的 mmap 映射是性能关键。第一次跑模型加载了一两分钟然后前几步去噪慢得离谱。后来发现是 mmap 的页缓存没有激活权重文件在 SSD 上被反复读取。解决方案是启动时把映射区域顺序读一遍预热import mmap with open(weight_path, rb) as f: with mmap.mmap(f.fileno(), length0, accessmmap.ACCESS_READ) as m: for i in range(0, size, 1024 * 1024): _ m[i] # 触发页缓存还有一个很多人忽略的点跑长任务前把云同步盘、后台备份、浏览器多开都收一收。我笔记本上的网盘客户端会周期性扫盘一旦和权重读取抢 IO采样速度直接腰斩。外接电源是必须的低电量模式下 MacBook 会主动降频性能掉 40% 都不止。如果内存压力已经标红唯一的有效手段是降档——换更小的分辨率、更少的帧数或者把权重从 Q6 换成 Q4。不要指望系统自己扛过去swap 一开速度就是断崖式下跌。4.3 采样参数与帧调度视频 DiT 和文生图在采样参数上有明显差异。CFG 过高是闪烁的元凶我试过 7.5 的 CFG画面单帧看还行连起来播放能看出明显的“呼吸感”——亮度和对比度在帧间抖动。实际测试下来 CFG 在 3.5 到 5.5 区间最稳。steps 方面25 步的收益已经接近饱和再往上加只是线性增加耗时画质提升几乎看不见。帧调度是我后期优化最大的收益点。一开始我试着让 C 引擎一次性处理全部 24 帧的 latent内存峰值直接顶到接近 60GB系统开始频繁 swap。改成逐帧采样后内存峰值稳在 52GB 左右而且还能在采样中途把前几帧提前送去 VAE 解码预览构图。这个“首帧预览”的 trick 很实用先生成 192×112 的 4 帧小图看整体构图和运动方向对不对再上全量分辨率能省下不少试错时间。另外latent 的尺寸必须是 VAE 压缩率的整数倍我这套模型是 8 倍下采样所以宽高按 16 的倍数设置最稳妥省得边缘像素出黑边。5. 常见问题速查表整合这一路的经验我把遇到的典型问题整理成速查表症状可能原因解决方法ComfyUI 节点列表里搜不到插件NODE_CLASS_MAPPINGS 没注册或init.py 没 import检查init.py 的导入和注册重启 ComfyUIEXC_BAD_ACCESS 段错误ctypes 没设置 argtypes指针被截断给每个 C 函数显式声明 argtypes / restype生成全黑帧或彩色噪点VAE 解码精度问题或 latent 缩放因子没还原VAE 用 fp32 解码检查 scale factor 是否正确归一化采样中途出现 NaN 花屏-ffast-math或 Q4 溢出或 CFG 过高去掉 fast-math降低 CFG换 seed 重试内存压力标红系统卡死权重加激活超限触发大量 swap减帧数、降分辨率、换更低量化档位关浏览器前几步去噪异常慢mmap 冷启动页缓存未激活启动时顺序读一遍权重文件预热MPS 和 C 引擎结果不一致dtype 或内存布局不一致确认两边都是 float16 contiguous加逐层校验几个独家避坑技巧不要相信活动监视器第一次显示的内存数字。mmap 的页缓存会被算进“已使用”里但这部分内存是可以在压力下来时让出去的真正要盯的是内存压力曲线和 swap 数值。所有指针地址在调试阶段都用hex()打一遍日志。C 侧崩了之后对比最后打印的地址和传入地址往往一眼就能看出问题。想排查画质问题别直接看视频。让 VAE 解码后输出 PNG 序列单独检查第 1 帧、第 8 帧、第 16 帧定位到底是采样问题还是时间维度的不一致。6. 没人写进 README 的工程经验6.1 瓶颈往往不在计算而在数据管道整个链路跑下来我最意外的结论是33B 模型在 MacBook 上的瓶颈根本不是“算力”而是内存带宽和权重读取效率。M3 Max 的 NEON/AMX 乘加能力应付这个规模的计算绰绰有余但每一步去噪都要把约 20GB 的 Q4 权重从头到尾扫一遍这成了时间的主要开销。所以量化带来的提速比超频明显得多也解释了为什么线程从 8 加到 12 反而变慢——大家都在抢内存带宽。这也意味着真正有效的优化方向只有一个减少每次去噪需要读的字节数。量化是第一层第二层是把 text encoder 和 VAE 的权重尽量留在 MPS 上、不占 C 引擎的带宽第三层是采样步数每少一步就少扫一遍权重。6.2 这个插件的边界在哪里明确说一下适用范围它适合 5 到 10 秒的短视频、采样器和量化研究、以及私有素材不出本机的场景。不适合长视频、大批量生产、以及需要严格保证同 seed 可复现的协作环境——不同机器、不同编译参数跑出来的像素级结果都会有差异。后续扩展方向我留了几个把 LoRA 权重直接 merge 进 C 侧权重文件这样单帧采样时间几乎不变支持输出 PNG 序列而不是 mp4方便后期调色和 alpha 合成把 text encoder 换成更小的模型省出来的内存还能把分辨率往上抬一档。最后说点真实的体会。我跑完这一圈最大的收获不是“33B 能上 Mac”这个结论而是把 DiT 前向、量化格式、CFG 采样和 ComfyUI 节点系统之间的关系彻底理清了。如果你也想复刻这条链路我的建议很直接别一上来就上 33B。先用一个 500M 左右的小模型把“ctypes 调 C 引擎 → ComfyUI 节点 → 出视频”这条管道跑通再换大模型调试成本至少省一半。