发生了什么:大模型「看权重」就治住了小模型的死循环

11 月 2 日,MIT CSAIL 的研究者发布了一篇题为 Sparks of Weight-Space Programming 的博客,在 Hacker News 上迅速冲到近两千分。核心结论很反直觉:前沿大模型(文中称为 GPT-6 Astra)不靠梯度下降、不跑训练循环,只是像读代码一样阅读神经网络的原始权重,就能判断网络学会了什么,甚至直接改写权重修 bug。

其中最适合动手复现的是「实验 8」:Qwen3.5-0.8B 在思考模式下很容易陷入「思考死循环」(Qwen 官方模型卡也专门提醒过这一点),一直重复直到耗尽 token 上限。Astra 在一个只能读写权重的 Python 环境里,把问题定位到第 15 层和第 19 层 MLP 写出的一个「重复方向」,然后从这两层 down_proj 各 64 个输出列中减掉它——总共只动了 131,072 个权重(约占全模型 0.016%)。

按论文公布的数据,修改后 MATH-500 从 7.9% 提升到 48.1%,GSM8K 从 13.8% 到 67.1%,指令遵循 IFEval 从 21.0% 到 62.3%,命中 token 上限的比例从 93% 降到约 33%。

更难得的是,作者把**完整日志和逐位精确的权重差分(diff)**全部开源了。本文就带你:

  1. 用官方脚本从原版 Qwen3.5-0.8B 重建「Astra 修复版」,并校验哈希;
  2. 自己写几行 numpy 确认到底改了哪些权重;
  3. 用 vLLM 把原版和修复版跑起来,亲眼对比「会不会陷入死循环」。

前置条件

项目说明
系统Linux / macOS 均可完成第 1、2 步(纯 CPU + numpy);第 3 步建议 Linux + NVIDIA GPU
Python3.10 及以上,需要 numpy、huggingface_hub
磁盘约 4 GB(原版权重约 1.75 GB,再生成一份修改后的权重)
推理(可选)能运行 Qwen3.5 的较新版 vLLM;Qwen 模型卡写明 Qwen3.5 需要较新的 vLLM / Transformers

只想先看效果?作者团队和 Hugging Face 的 akhaliq 做了一个在线 Demo(见文末参考来源),可以输入题目,同时观察原版和修复版的输出。

第一步:拉取日志仓库和原版权重

修复是基于特定版本的 Qwen3.5-0.8B 做的,官方脚本会校验基座张量的 sha256,所以下载时一定要锁定 revision:

# 1. 克隆实验日志(含权重 diff 和重建脚本)
git clone --depth 1 https://github.com/RikiyaT/lm_understand-logs.git

# 2. 安装依赖
pip install -U numpy huggingface_hub

# 3. 下载指定 revision 的 Qwen3.5-0.8B(只需要单个 safetensors 文件)
hf download Qwen/Qwen3.5-0.8B \
  model.safetensors-00001-of-00001.safetensors \
  --revision 2fc06364715b967f1860aea9cf38778875588b17 \
  --local-dir ./qwen35-base

国内网络如果下载慢,可以先设置镜像环境变量 HF_ENDPOINT 再执行下载。

修复相关的文件在 exp8_math/runs/p50c/edit/ 下:

  • manifest.json:被修改的张量名、形状、改动条目数,以及修改前后的 sha256;
  • edit.npz:被修改位置的索引和新的 bfloat16 数值。

p50c 对应论文里「允许 50 次前向推理」那一轮,也就是效果最好的版本。另外两轮 p10c、p25c 的修法完全不同(一个是对 30 个矩阵做秩一修改,一个是直接关掉第 15 层的 7 号注意力头),感兴趣可以对照 final_answer.md 阅读。

第二步:用官方脚本重建修复版并校验

apply_edit.py 只依赖 numpy(仓库里自带一个极简的 safetensors 读写器 st_numpy.py),不需要 PyTorch,也不需要 GPU:

python lm_understand-logs/exp8_math/code/apply_edit.py \
  qwen35-base/model.safetensors-00001-of-00001.safetensors \
  lm_understand-logs/exp8_math/runs/p50c/edit \
  edited.safetensors

脚本对每个张量做两次断言:先确认你的基座张量哈希与作者使用的一致,再确认重建后的张量哈希与论文评测时的检查点一致。成功时会看到类似输出:

model.language_model.layers.15.mlp.down_proj.weight: rebuilt (sparse, 65,260 entries changed), hash OK
model.language_model.layers.19.mlp.down_proj.weight: rebuilt (sparse, 64,928 entries changed), hash OK
2 tensors -> edited.safetensors

注意第三个参数如果以 .safetensors 结尾,输出的是完整权重文件;否则会输出成一个文件夹,里面每个被改的张量各一个 .npy。

第三步:自己验证「到底改了哪里」

别只信脚本输出,写几行代码亲手比对一遍,顺便理解这次修改的「形状」:

# diff_check.py —— 在包含 lm_understand-logs、qwen35-base、edited.safetensors 的目录下运行
import sys, numpy as np
sys.path.insert(0, "lm_understand-logs/exp8_math/code")
import st_numpy as S

base = S.load("qwen35-base/model.safetensors-00001-of-00001.safetensors")
edit = S.load("edited.safetensors")

total = 0
for name, (dt, a) in base.items():
    b = edit[name][1]
    diff = a != b                      # bf16 以 uint16 位模式比较
    n = int(diff.sum())
    if n:
        cols = int(diff.any(axis=0).sum())
        print(f"{name}: shape={a.shape}, 改动 {n:,} 个值, 涉及 {cols} 列")
        total += n
print("合计改动:", f"{total:,}")

你应该只看到第 15、19 层的 mlp.down_proj.weight 两个张量(形状 1024×3584),每个都只涉及 64 列。这与 manifest.json 中记录的数据一致:两层合计触碰 131,072 个权重,其中 130,188 个在四舍五入到 bfloat16 后确实与原值不同。

为什么改 down_proj 的「列」?down_proj 负责把 MLP 中间层的神经元激活写回残差流,每一列对应一个中间神经元的「写出方向」。从这些列中减掉「重复」方向,相当于让这 64 个神经元不再往残差流里推动模型「再说一遍」。

第四步:用 vLLM 对比原版与修复版

如果不想自己拼装,作者已经把重建好的完整模型放到 Hugging Face:RikiyaT/Qwen3.5-0.8B-weight-space-programming(只有上述两个张量与原版不同,模型卡上列了哈希)。用两个端口分别启动原版和修复版:

vllm serve Qwen/Qwen3.5-0.8B --port 8000 --max-model-len 34816
vllm serve RikiyaT/Qwen3.5-0.8B-weight-space-programming --port 8001 --max-model-len 34816

然后用 OpenAI 兼容接口发同一道题,采样参数与论文评测一致(思考模式、temperature 0.6、top_p 0.95、top_k 20、presence_penalty 1.5):

# compare.py
from openai import OpenAI

Q = ("一个水池有进水管和出水管,单开进水管 6 小时注满,单开出水管 9 小时放空。"
     "两管同时打开,几小时注满?\n"
     "Please reason step by step, and put your final answer within \\boxed{}.")

for port, model in [(8000, "Qwen/Qwen3.5-0.8B"),
                    (8001, "RikiyaT/Qwen3.5-0.8B-weight-space-programming")]:
    client = OpenAI(base_url=f"http://localhost:{port}/v1", api_key="EMPTY")
    r = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": Q}],
        max_tokens=32768,
        temperature=0.6, top_p=0.95, presence_penalty=1.5,
        extra_body={"top_k": 20,
                    "chat_template_kwargs": {"enable_thinking": True}},
    )
    c = r.choices[0]
    print(f"== {model}")
    print("finish_reason:", c.finish_reason,
          "| completion_tokens:", r.usage.completion_tokens)
    print((c.message.content or "")[-300:])

重点看两项:finish_reason 为 length 说明模型一直写到上限都没收尾(典型的死循环);stop 才是正常结束。单道题有随机性,建议换十几道题、多跑几次再下结论。模型卡里也提供了基于 vllm.LLM 的离线推理写法,批量评测时更方便。

常见坑

  1. 基座版本不对,哈希断言失败。 没加 --revision 下载到的可能是更新过的权重,apply_edit.py 会直接报「the base tensor differs」。锁定 2fc0636… 这个提交即可。
  2. 以为这是「全面增强」。 这次修复本质上是去掉阻碍模型给出答案的循环,而不是凭空增加数学能力。论文的补充实验里,几乎不陷入循环的 Gemma 3 270M 用同样方法就没有明显提升——作者明确写到,编辑权重能移除「遮住能力的障碍」,但加不了模型本来没有的能力。
  3. vLLM / Transformers 版本太旧。 Qwen3.5 是新架构,旧版本会报不支持的模型类型。按 Qwen 模型卡的要求升级到较新版本。
  4. max-model-len 设太小。 评测协议允许最多 32,768 个新 token,上下文开太小会让两个模型都提前被截断,看不出差别。
  5. 采样参数不一致。 对比时两边务必使用同一组参数,尤其是 presence_penalty;Qwen 官方也提到调大该参数本身就能缓解重复,混用会让结论失真。
  6. 仍会有错误和退化。 Astra 在最终报告里自己也写了:数学错误、缺少 \boxed{}、部分输出退化依然存在。别把 0.8B 小模型当成可靠的解题器。

总结

这次实验的意义不在于又多了一个「数学更好」的小模型,而在于大模型开始能直接读写神经网络权重:定位问题、给出可解释的修改、只动极少量参数。对普通开发者而言,最大的价值是它完全可复现——一份锁定版本的基座、一个 numpy 脚本、两组哈希,就能逐位还原论文里的检查点,再用 vLLM 亲眼看到死循环消失。

如果你在本地或边缘设备上部署 Qwen3.5-0.8B 并苦于思考模式「停不下来」,这个修复版值得纳入测试;如果你做可解释性或模型安全,作者公开的 1,125 轮完整对话日志更是一份难得的一手材料。

参考来源