发生了什么:大模型「看权重」就治住了小模型的死循环
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)**全部开源了。本文就带你:
- 用官方脚本从原版 Qwen3.5-0.8B 重建「Astra 修复版」,并校验哈希;
- 自己写几行 numpy 确认到底改了哪些权重;
- 用 vLLM 把原版和修复版跑起来,亲眼对比「会不会陷入死循环」。
前置条件
| 项目 | 说明 |
|---|---|
| 系统 | Linux / macOS 均可完成第 1、2 步(纯 CPU + numpy);第 3 步建议 Linux + NVIDIA GPU |
| Python | 3.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 的离线推理写法,批量评测时更方便。
常见坑
- 基座版本不对,哈希断言失败。 没加
--revision下载到的可能是更新过的权重,apply_edit.py会直接报「the base tensor differs」。锁定2fc0636…这个提交即可。 - 以为这是「全面增强」。 这次修复本质上是去掉阻碍模型给出答案的循环,而不是凭空增加数学能力。论文的补充实验里,几乎不陷入循环的 Gemma 3 270M 用同样方法就没有明显提升——作者明确写到,编辑权重能移除「遮住能力的障碍」,但加不了模型本来没有的能力。
- vLLM / Transformers 版本太旧。 Qwen3.5 是新架构,旧版本会报不支持的模型类型。按 Qwen 模型卡的要求升级到较新版本。
max-model-len设太小。 评测协议允许最多 32,768 个新 token,上下文开太小会让两个模型都提前被截断,看不出差别。- 采样参数不一致。 对比时两边务必使用同一组参数,尤其是
presence_penalty;Qwen 官方也提到调大该参数本身就能缓解重复,混用会让结论失真。 - 仍会有错误和退化。 Astra 在最终报告里自己也写了:数学错误、缺少
\boxed{}、部分输出退化依然存在。别把 0.8B 小模型当成可靠的解题器。
总结
这次实验的意义不在于又多了一个「数学更好」的小模型,而在于大模型开始能直接读写神经网络权重:定位问题、给出可解释的修改、只动极少量参数。对普通开发者而言,最大的价值是它完全可复现——一份锁定版本的基座、一个 numpy 脚本、两组哈希,就能逐位还原论文里的检查点,再用 vLLM 亲眼看到死循环消失。
如果你在本地或边缘设备上部署 Qwen3.5-0.8B 并苦于思考模式「停不下来」,这个修复版值得纳入测试;如果你做可解释性或模型安全,作者公开的 1,125 轮完整对话日志更是一份难得的一手材料。
参考来源
- Sparks of Weight-Space Programming: LLMs can natively read & write neural network weights(原文博客)
- RikiyaT/lm_understand-logs:完整实验日志、权重 diff 与重建脚本
- Hugging Face:RikiyaT/Qwen3.5-0.8B-weight-space-programming(修复版模型)
- Hugging Face Space:weight-space-programming-demo(在线对比 Demo)
- Hugging Face:Qwen/Qwen3.5-0.8B 官方模型卡
- Hacker News 讨论:Astra can read and write weights to improve Qwen