本文来自社区投稿,作者风外听竹
Intern‑S2‑Preview 是由上海人工智能实验室开源发布,面向科学场景的多模态大模型。模型文本主干基于 Qwen3.5-MoE,同时还带有视觉编码器和一个 time_series 时间序列模块。我的目标是借助 oMLX 的 oQ(Optimal Quantization) 将模型压缩至 4bit。
在实践过程中,主要经历了三步关键操作:探查 oMLX 的量化逻辑、对 oq.py 打补丁以打通灵敏度测量、以及处理 511 个孤立参数以让推理顺利运行,每一步都深入触及了 oMLX 的源码核心。本文将结合我的实战经验,对这些技术细节进行系统分享,希望对大家有所帮助。
1.探查 oMLX 的量化逻辑
当我用 oQ 对 Intern-S2-Preview 做量化时,直接遇到了一条报错:
intern_s2_preview model type not supported for auto-proxy sensitivity
intern_s2_preview 不在 mlx‑lm 的类型注册表里。
打开 config.json:
{"model_type": "intern_s2_preview","text_config": {"model_type": "qwen3_5_moe_text"},"vision_config": { ... }}
可以看到外层的 model_type 是 intern_s2_preview,但文本主干是 qwen3_5_moe。量化只关心文本层的权重分布------灵敏度测量的对象就是那 40 层 Qwen3.5 MoE。
于是问题变成:如何让 oQ 在测量灵敏度时把 intern_s2_preview 当成 qwen3_5_moe?
深入 oMLX 源码:oQ 的灵敏度测量链路
oMLX 的量化核心在 oq.py。关键函数链:
quantize_oq_streaming()
→ _measure_sensitivity() # 灵敏度测量入口
→ 检测 config 中是否有 vision_config → 判断 is_vlm
→ VLM 路径:mlx_vlm.utils.load_model()
→ 非 VLM 路径:mlx_lm.load() → _get_classes(config) → MODEL_REMAPPING
→ _measure_sensitivity_from_quantized_model() # 备选路径(已量化模型做 proxy)
关键发现:
-
VLM 检测机制 :oQ 通过 config 中的
vision_config键来判断模型是否 VLM。Intern-S2-Preview 的 config.json 有vision_config,但_measure_sensitivity中实际的判定逻辑走的是非 VLM 分支(因为is_vlm设为 False,原因是在更早的配置处理中剥离了视觉部分)。 -
MODEL_REMAPPING 字典 :mlx-lm 的
utils模块维护了一个MODEL_REMAPPING字典,将不认识的模型类型映射到已知类型。oMLX 在oq.py第 1591 行硬编码了对deepseek_v4的支持------这就是我们的注入点模板。 -
config.json 磁盘读取问题 :
mlx_lm.load()从磁盘读取config.json,即使你在内存中修改了配置,它仍然读到磁盘上原始的intern_s2_preview。这意味着 monkey‑patch 必须在load()调用之前注入,加载完成后恢复。 -
双灵敏度函数 :代码里存在两个灵敏度测量相关函数------主入口
_measure_sensitivity和量化后的备选_measure_sensitivity_from_quantized_model。两者都可能绕过你的补丁,必须同时覆盖。 -
内置标定数据:oQ 自带 560 条 code_multilingual 文本(704 KB),用于模型前向传播采样。实际只用 2 samples × 128 tokens。这意味着不需要额外准备标定数据集。
-
灵敏度分层:40 层测下来,L0(第一层)灵敏度 0.0055 最高,L13=0.0005 最低。灵敏度越高的层,量化时保留的精度越高。这个分布本身就印证了 Qwen3.5 MoE 的层间重要性差异。
web 搜索的辅助发现
过程中检索了 oMLX GitHub issues:
-
#1030:
nemotronh_nano_omni_reasoning_v3同样报 "model type not supported" -
#111:
qwen3_tts相同问题 -
#554:
gemma4在灵敏度测量中不支持 -
v0.3.9 发布笔记提到了 "auto‑build proxy model" 和 "mlx‑lm patched in oQ auto‑built sensitivity proxy"
还有一个 HuggingFace 线索:chanderbalaji/Intern‑S2‑Preview‑FP8‑MLX‑4bit 已经有人做过 MLX 4bit 转换。这说明社区在用笨办法绕过------先转标准格式。
2.对 oq.py 打补丁以打通灵敏度测量
有了上述理解,补丁方案就清晰了:
核心补丁逻辑
在 _measure_sensitivity 的 lm_load 调用前,注入 MODEL_REMAPPING:
_need_monkey = (
config.get("model_type") == "intern_s2_preview"and not is_vlm
)
if _need_monkey:
# 保存原始映射
_orig_remapping = dict(getattr(_mlx_utils, "MODEL_REMAPPING", {}))
# 注入临时映射
_mlx_utils.MODEL_REMAPPING["intern_s2_preview"] = "qwen3_5_moe"
logger.info("oQ: monkey-patched MODEL_REMAPPING for intern_s2_preview")
# ... lm_load() 调用 ...if _need_monkey:
# 恢复原始映射,不留副作用if "intern_s2_preview" in _orig_remapping:
_mlx_utils.MODEL_REMAPPING["intern_s2_preview"] = _orig_remapping["intern_s2_preview"]
else:
_mlx_utils.MODEL_REMAPPING.pop("intern_s2_preview", None)
logger.info("oQ: restored MODEL_REMAPPING after sensitivity load")
踩过的坑
-
VLM 分支遗漏 :最初只给非 VLM 分支加了
strict=False,但 oQ 的实际代码路径走的是 VLM 分支(mlx_vlm.load),导致补丁白打。必须两个分支都改。 -
strict 模式 :
mlx_vlm.load_model(..., strict=False)和mlx_lm.load(..., lazy=True)都需要显式传参,否则孤儿参数(time_series 模块)会导致 load 失败。 -
多次迭代 :补丁 → 测试 → 日志显示 monkey‑patch 生效但加载仍失败 → 检查分支 → 修复 → 再测试。最终在第 48 轮得到
SUCCESS after 73s: 40 layers measured。
同步到 App
补丁写好后,同步到 /Applications/oMLX.app/Contents/Resources/omlx/oq.py,重新构建。经过多轮调试(包括僵尸进程、端口冲突等物理世界的混乱),最终 oMLX 的 GUI 量化管线也跑通了。
3.剥离 511 个孤儿参数以让推理顺利运行
量化成功。推理启动,然而又遇到了报错:
Received 511 parameters not in model
全部来自 language_model.model.time_series.encoder.*------153 weight + 133 bias + 112 scales + 112 biases + 1 in_proj_bias = 511 个。
原因分析
oMLX 的 MTP(Multi‑Token Prediction)推理运行时是 qwen35_moe_vlm_runtime.py,仅 446 行。它知道 Qwen3.5 MoE 的每一层------但不认识 time_series。而 mlx_lm.load(strict=True) 不允许孤儿参数。
解决方案
-
补 runtime------给 446 行 runtime 加 time_series 模块 → 代价最高
-
改 loading 为 strict=False ------ oMLX 底层可能不支持
-
从 safetensors 中剥离 time_series 参数 → 最快,对推理零影响
最终选择了方案3。
处理过程
_need_monkey = (
config.get("model_type") == "intern_s2_preview"and not is_vlm
)
if _need_monkey:
# 保存原始映射
_orig_remapping = dict(getattr(_mlx_utils, "MODEL_REMAPPING", {}))
# 注入临时映射
_mlx_utils.MODEL_REMAPPING["intern_s2_preview"] = "qwen3_5_moe"
logger.info("oQ: monkey-patched MODEL_REMAPPING for intern_s2_preview")
# ... lm_load() 调用 ...if _need_monkey:
# 恢复原始映射,不留副作用if "intern_s2_preview" in _orig_remapping:
_mlx_utils.MODEL_REMAPPING["intern_s2_preview"] = _orig_remapping["intern_s2_preview"]
else:
_mlx_utils.MODEL_REMAPPING.pop("intern_s2_preview", None)
logger.info("oQ: restored MODEL_REMAPPING after sensitivity load")
步骤:
-
备份原始分片和 index
-
遍历 safetensors header,过滤
time_series键 -
保留非 time_series 的 tensor 数据,重写分片
-
更新 model.safetensors.index.json(2230 → 1719)
-
校验一致性(index 中的每个权重都能在 shard 中找到)
复用脚本
~/.lmstudio/scripts/strip_time_series.py------传入模型目录,自动完成上述五步。dry‑run 模式先预览,确认后正式执行。后续 5bit/6bit 量化产物直接喂给它。
复盘总结
Intern‑S2-Preview 是一个异构架构------同一个 safetensors 文件里混着三种模块的权重:
|----------------|-----------------|-----------------------|
| 模块 | 类型 | oMLX runtime 是否支持 |
| language_model | Qwen3.5 MoE(文本) | √ 支持 |
| vision_model | 视觉编码器 | × 但 oQ 剥离了 |
| time_series | 时间序列 | × 完全未知 |
oMLX 的 oQ 管线只处理文本层(language_model),但 quantize 阶段不剥离不认识的权重------它忠实地把整个 safetensors 量化后原样输出。推理时 runtime 按 strict=True 加载,遇到孤儿参数就崩。
经验教训:
-
异构模型的量化产物不能直接喂给统一 runtime------量化前要了解 runtime 的模块清单
-
mlx‑lm 的 MODEL_REMAPPING 是可插拔的扩展点,不用改 mlx‑lm 自身,在 oq.py 里 monkey‑patch 即可
-
safetensors 是自描述的,header 包含所有 tensor 名,可以直接做权重的增删改,不需要理解模型结构
-
oQ 内置标定数据(560 条 code_multilingual)对大多数模型够用,不需要额外准备