由于本人的 2060 显卡运损,我之前运行的一系列本地 AI 工具尽数不可用,不得不临时使用我的 MacBook 运行它们,所幸他们大部分使用的是 PyTorch 框架,简单修改 device=mps 即可成功运行

不过在后续的使用过程中,发现它的效率远不如之前的 2060,同时会导致我的 MacBook 发热严重(没有主动散热的 Air 版本),系统卡顿,我不得不另寻他法

发现 MLX

在查阅 stable-ts 项目的文档时,意外发现它已有 MLX (Apple Machine Learning Exploration) 框架的支持

为了利用这一性能优势,我根据文档修改了模型加载函数。然而,在实际运行我本地下载的 Hugging Face 模型 kotoba-whisper-v2.0 时,程序却抛出了一个错误:

ModelDimensions.__init__() got an unexpected keyword argument '_name_or_path'

意思是 PyTorch 的模型,其内部参数结构(如 _name_or_path)与 MLX 框架的模型定义并不直接兼容

从 PyTorch 到 MLX

解决这个问题的核心步骤是将现有的 Whisper 模型权重转换为 MLX 框架能够识别和加载的格式,幸运的是 MLX 社区提供的转换脚本:convert.py

这个脚本负责解析原始 Whisper 模型的权重文件,并将其重新保存为 MLX 特定的格式,确保所有张量和配置项都能被 MLX 库正确加载和初始化。By the way,如果使用的是 OpenAI 官方开源的 Whisper 模型,MLX 包内已预先提供了一些转换好的模型地址,可以直接使用,省去了手动转换的麻烦

性能飞跃

完成模型转换并成功运行后,MLX 相较于 whisper.cpp 实现效率提升了 2 到 5 倍。更重要的是,在 Apple Silicon 芯片上,其功耗使用基本完全集中在 Apple Neural Engine (ANE) 上,使得 GPU 的负载极低,几乎不产生额外负担

在 Real_ERSGAN 上使用 CoreML

在 whisper 上面使用 mlx 为我提供了一个新的思路,我是否可以在其他 AI 应用上面也使用 ANE 代替 GPU 呢?

于是我找到了我用于超分的工具 Real_ERSGAN,并在尝试在它上面使用 CoreML

可行性及方案

与 whisper 不同,Real_ERSGAN 并没有已经写好了的 mlx 工具库,需要自行开发,而我也完全没有 PyTorch 和 CoreML 的开发经验,我所想的便是求助于 AI,这里我问的是 Gemini 2.5 Pro

我现在正在使用mps运行 Real-ESRGAN超分视频,但是我觉得它运行地太慢了,我希望使用 apple core ml进行优化,这是否可行

当然可行!而且这是一个非常好的优化思路。将 Real-ESRGAN 模型从单纯使用 PyTorch + MPS 切换到使用 Core ML 进行推理,通常会带来显著的性能提升和能效改进。

为什么 Core ML 会比 MPS 更快?

你目前使用的 device='mps' 是让 PyTorch 框架通过 Metal Performance Shaders (MPS) 后端来利用 Apple Silicon 的 GPU。这已经是一种很好的硬件加速方式了,但它有其局限性:

  1. MPS 是一个通用后端:它为 PyTorch 这样的框架提供了一个在 Metal API 上运行计算图的“翻译层”。它负责将 PyTorch 的操作(如卷积、激活函数等)映射到 GPU 上执行。
  2. Core ML 是一个专用的推理框架:Core ML 是苹果官方为旗下所有设备(Mac, iPhone, iPad)打造的机器学习推理框架。它的设计目标就是最大化地利用苹果硬件的特性。

使用 Core ML 的核心优势在于:

  • 调用神经网络引擎 (ANE - Apple Neural Engine):这是关键!Apple Silicon 芯片中包含一个专门为机器学习计算设计的硬件单元——ANE。ANE 在执行神经网络的矩阵乘法和卷积等操作时,速度极快且功耗极低。
  • 图级别优化 (Graph-Level Optimization):在将模型转换为 .mlpackage 格式时,coremltools 会对整个计算图进行深度优化,比如层融合、算子重排等,这些优化可以减少内存读写和计算开销。

如何操作:将 Real-ESRGAN 转换为 Core ML 模型

步骤 1: 转换 PyTorch 模型为 Core ML 格式

你需要使用苹果官方提供的 coremltools Python 包来完成转换。

…具体的转换脚本

步骤 2: 在视频处理脚本中调用 Core ML 模型

现在你有了 .mlpackage 文件,你需要修改你原来的视频超分脚本,将 PyTorch 模型的推理部分替换为 Core ML 模型的推理。

…具体的模型调用

结论:

你的想法完全正确且非常值得尝试。将 Real-ESRGAN 转换为 Core ML 格式是压榨你 Mac 性能、大幅提升视频超分速度和效率的最佳途径。按照上面的步骤操作,你将能体验到 ANE 带来的强大加速效果。

模型转换

后续的开发工作都是 Cursor 上面完成

我将 Gemini 提供的代码贴到了 Real_ERSGAN 的代码仓库中,然后运行报错一气呵成

$ python mlx/convert.py
...
开始转换模型到 Core ML...
/opt/homebrew/Caskroom/miniforge/base/envs/real-esrgan/lib/python3.11/site-packages/coremltools/converters/mil/mil/passes/defs/preprocess.py:273: UserWarning: Output, '4053', of the source model, has been renamed to 'var_4053' in the Core ML model.
正在进行 FP16 量化...
Quantizing using linear quantization
Traceback (most recent call last):
...
Exception: MLModel of type mlProgram cannot be loaded just from the model spec object. It also needs the path to the weights file. Please provide that as well, using the 'weights_dir' argument.

简单来说是 quantize_weights 旧版量化 API 与新的 mlprogram 格式不兼容,导致转换脚本报错,Cursor 在转换时直接指定精度 compute_precision=ct.precision.FLOAT16 成功将 RealESRGAN_x4plus 转换成了 mlprogram 格式

当在修改后的 inference_realesrgan_video.py 上面调用新模型时,运行了几步之后又有新的报错

NSLocalizedDescription = "Image size 266 x 266 not in allowed set of image sizes";

这次的问题出在视频分块(Tiling)上,我的模型被转换成了固定的 256x256 输入,但处理视频边缘的图块时,尺寸并不总是刚好 256x256

虽然 Cursor 建议使用 RangeDim 解决,但是鉴于 Strange issue: Converting RealESRGAN to CoreML model, ML Program format performs significantly slower than Neural Network format #2545,我还是放弃了可变尺寸,让 Cursor 重构了推理逻辑,自动将边缘的小图块补齐到模型所需的 256x256,推理后再裁剪掉多余部分

色彩映射

解决完模型本身的问题后,可算是可以运行了,我激动地打开输出的视频,结果……一片漆黑,这种没有报错的 bug 是真的棘手

不过 Cursor 似乎见怪不怪,在后处理函数 post_process 里加上调试代码,然后重新运行

[Debug] Output type: <class 'numpy.ndarray'>
[Debug] Output shape: (1, 3, 1024, 1024), dtype: float32
[Debug] Output range: [-22.4844, 23.4688]

日志中可见,模型输出的值域完全不对,正常应该在 ([0, 1]) 之间。这导致所有像素在转换成 uint8 时都被裁剪成了 0(黑色)

Cursor 在转换脚本中放弃 ImageType,改用更底层的 TensorType,同时,在推理脚本中完整地实现了手动的归一化(输入前)和反归一化(输出后),彻底掌控了数据流

重新运行脚本视频终于能正常输出了,但新的问题又来了:

  1. 用 FP16 模型处理时,画面出现严重的颜色失真(偏红)
  2. 两种模型画面都会整体偏暗

Cursor 的解决方案

  1. FP16 颜色问题:修改转换脚本,确认使用 compute_precision=FLOAT16 在转换时直接生成 FP16 模型(之前是 ct.precision.FLOAT16
  2. 偏暗问题
  • 对于 FP32,原因是 astype(np.uint8) 的直接截断行为,将其改为 np.round(...).astype(np.uint8),通过四舍五入解决了亮度损失
  • 对于 FP16,调试信息显示仍有 6.72% 的像素值为负数,Cursor 实现了一个动态范围拉伸算法,在后处理时检测到负值偏移,就自动将 ([-0.1478, 1.0586]) 这样的范围线性拉伸回 ([0, 1])

至此,从一个初步的想法,到解决 API 兼容、数据类型、尺寸、数据范围、视觉质量等一系列问题,我们终于成功地将脚本迁移到了 Core ML,并获得了理想的效果:

在 FP32 下,RealESRGAN_x4plus 从 8 s/frame 提升到 1.25 s/frame,realesr-general-x4v3 从 3 frame/s 提升到 8 frame/s,这个数据

running real_esrgan with coreml

不过美中的是,使用 FP16 对比 FP32 没有获得额外的效率提升

总结

这次折腾下来,感觉收获还是挺多的。最初只是因为 Macbook 跑 PyTorch+MPS 发烫,想找个替代方案,没想到从 Whisper 的 MLX 实践,一路摸索到了 Real-ESRGAN 的 Core ML 改造

每次 Apple 的新的 M-Series 发布都会提到他们的 ANE 又提升了多少,而这是我第一次切身体会到 ANE 的强大所在。 虽然 FP16 没比 FP32 快多少这个小遗憾还在,但这已经为我打开了一扇新的大门

这次和 LLM 一起探索一个新领域也让我有了新的体会。我提出一个模糊的想法,它能迅速给出可行的技术路线;我扔给它一堆报错,它能一步步定位问题。我只需要负责验证、反馈,然后就能一起把问题解决掉。整个过程,我更像一个产品或者测试,而它才是真正的研发