1. 为什么读 DeepSeek-V3 权重转换脚本会卡住
DeepSeek-V3 的 FP8 转 BF16 脚本,核心逻辑其实不复杂:读model.safetensors.index.json拿到weight_map,遍历每个 safetensors 分片,遇到element_size() == 1的权重就去找对应的*_scale_inv,调用weight_dequant做去量化,最后把scale_inv条目从索引里 pop 掉再写回。但真正逐行读的时候,卡点往往不在语法,而在几个隐式约定上。
第一个卡点是scale_inv和weight_map的对应关系。get_tensor不是从当前分片里取,而是拿weight_map[tensor_name]去查这个张量到底在哪个文件,再从loaded_files缓存里取。也就是说,weight和它的scale_inv很可能不在同一个分片里,你如果只盯着当前current_state_dict找,永远找不到。
第二个卡点是weight.element_size() == 1这个判定。FP8 是 1 字节,BF16 是 2 字节,所以这个判断等价于「这是不是 FP8 权重」。但_scale_inv本身也是 1 字节,所以前面必须先continue掉endswith("_scale_inv"),否则会拿 scale 去当权重处理。
第三个卡点是loaded_files只保留最近两个分片。这个设计是为了显存,但读代码时容易误以为所有分片都在内存里,导致对get_tensor的查找范围判断错误。
我试过最笨的办法是手动比对model.safetensors.index.json里的weight_map,一行行对哪个权重在哪个文件、它的scale_inv又在哪个文件。这个方法能读懂,但效率极低,而且一旦模型有几百个分片,人眼根本对不过来。更实际的做法是:把 Codex 接到 TaoToken 上,边读源码边让它解释每一段的索引流向,你负责验证它说的对不对。
这篇就是讲怎么把 Codex 的 Base URL 改到 TaoToken,然后用它来逐行对照weight_dequant前后的逻辑,包括get_tensor的缓存分支、endswith("_scale_inv")的跳过判断、以及最后pop掉scale_inv再json.dump的那段。TaoToken 在这里只做模型通道,FP8 转换、CUDA 加载、索引写回全部由你本地脚本执行,它不碰任何张量运算。
2. 把 Codex 接到 TaoToken:先拿 Key 再改 Base URL
这一步的目标很简单:让 Codex 能正常发出请求,并且请求走 TaoToken 的模型通道。你不需要改本地脚本的任何转换逻辑,只改 Codex 的接入配置。
先打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册账号。注册完进控制台,创建一个 API Key。这个 Key 就是后面 Codex 要用的那把,创建后先复制出来,后面配置里要填。
创建 Key 的入口在控制台的 API Keys 页面,直接访问 https://taotoken.net/console/api-keys 也能到。Key 只显示一次,复制后先存到安全的地方。
然后改 Codex 的 Base URL。这里要注意:填https://taotoken.net/api,不带/v1,也不加任何 UTM 参数。很多人习惯性补/v1,结果请求路径对不上,直接 404。Key 就用刚创建的那把。
配置改完后,Codex 发出的请求就会走 TaoToken 的模型通道。你本地那个 FP8 转 BF16 的脚本完全不动,weight_dequant、torch.cuda.empty_cache()、json.dump都还是本地执行。TaoToken 只负责把 Codex 的对话请求转给模型,不参与任何权重计算。
如果你后面要长期用 Codex 做源码分析或者 Agent 类任务,可以看下 Coding Plan 页面 https://taotoken.net/coding-plan ,它更适合高频编码场景。只是临时读一段脚本的话,按量用 API 就够了。
3. 可复制配置:Codex 接入参数与本地脚本对照
3.1 Codex 侧配置
Codex 的配置通常在一个 JSON 或 TOML 文件里,具体路径看你用的版本。核心是两项:Base URL 和 API Key。下面是一个通用示例,字段名以你实际用的 Codex 版本为准。
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你刚创建的那把Key", "model": "deepseek-v3" }注意base_url结尾没有/v1,也没有斜杠。如果你用的 Codex 版本要求填完整 endpoint,就填https://taotoken.net/api,不要自己拼/v1/chat/completions,让 Codex 自己处理路径。
模型名按你实际要用的填。读 DeepSeek-V3 源码时,用 DeepSeek-V3 本身或者任意一个擅长代码的模型都行,关键是通道要通。
3.2 本地脚本侧:不改转换逻辑,只加对照点
本地脚本还是原来那份,main(fp8_path, bf16_path)的流程不变。你要做的是在几个关键位置停下来,把代码片段贴给 Codex 让它解释。下面把三个最值得对照的位置标出来。
第一个位置是get_tensor的缓存分支:
def get_tensor(tensor_name): file_name = weight_map[tensor_name] if file_name not in loaded_files: file_path = os.path.join(fp8_path, file_name) loaded_files[file_name] = load_file(file_path, device="cuda") return loaded_files[file_name][tensor_name]这里的关键是weight_map[tensor_name]决定了去哪个文件找。scale_inv的file_name和权重的file_name可能不同,所以不能只在current_state_dict里找。
第二个位置是跳过判断和去量化分支:
for weight_name, weight in current_state_dict.items(): if weight_name.endswith("_scale_inv"): continue elif weight.element_size() == 1: scale_inv_name = f"{weight_name}_scale_inv" try: scale_inv = get_tensor(scale_inv_name) fp8_weight_names.append(weight_name) new_state_dict[weight_name] = weight_dequant(weight, scale_inv) except KeyError: print(f"Warning: Missing scale_inv tensor for {weight_name}, skipping conversion") new_state_dict[weight_name] = weight else: new_state_dict[weight_name] = weightendswith("_scale_inv")先跳过 scale 本身,element_size() == 1再判定 FP8 权重,然后才去get_tensor拿 scale。KeyError捕获的是weight_map里根本没有这个scale_inv_name的情况,不是文件读不到。
第三个位置是索引写回:
for weight_name in fp8_weight_names: scale_inv_name = f"{weight_name}_scale_inv" if scale_inv_name in weight_map: weight_map.pop(scale_inv_name) with open(new_model_index_file, "w") as f: json.dump({"metadata": {}, "weight_map": weight_map}, f, indent=2)只有成功转换过的权重(进了fp8_weight_names)才会去 pop 它的scale_inv。跳过的权重,它的scale_inv条目还留在weight_map里。这一点很容易读漏。
3.3 参数对照表
| 配置项 | 值 | 说明 |
|---|---|---|
| Codex Base URL | https://taotoken.net/api | 不带/v1,不加 UTM |
| Codex API Key | 控制台创建的那把 | 只显示一次,先存好 |
| 本地脚本输入 | --input-fp8-hf-path | FP8 权重目录,含 index.json |
| 本地脚本输出 | --output-bf16-hf-path | BF16 权重输出目录 |
| 转换函数 | weight_dequant(weight, scale_inv) | 本地 kernel 执行,不走网络 |
| 显存管理 | loaded_files保留 2 个 +empty_cache() | 本地 CUDA 行为 |
4. 验证请求:让 Codex 复述一条完整权重路径
配置改完后,先别急着跑整个转换。先用一个最小验证确认通道真的通了:让 Codex 复述一条权重从 FP8 到 BF16 的完整路径。如果它能准确说出每一步的索引变化,说明 Codex 确实读到了你贴的代码,通道没问题。
你可以这样问 Codex:
下面这段是 DeepSeek-V3 FP8 转 BF16 脚本的核心逻辑。 请复述一条名为 model.layers.0.self_attn.q_proj.weight 的权重, 从 FP8 到 BF16 的完整路径,包括: 1. 它在 weight_map 里对应的 file_name 怎么查 2. 它的 scale_inv 怎么被 get_tensor 找到 3. weight_dequant 之后 new_state_dict 里存的是什么 4. 最后 weight_map 里哪个条目被 pop 掉把第 3 节里的三段代码贴进去。如果 Codex 能说出「先查weight_map["model.layers.0.self_attn.q_proj.weight"]得到分片文件名,再用weight_map["model.layers.0.self_attn.q_proj.weight_scale_inv"]查 scale 所在分片,get_tensor从缓存或磁盘取 scale,weight_dequant返回 BF16 张量存入new_state_dict,最后weight_map里 pop 掉..._scale_inv」,那说明它真的读懂了索引对应关系。
如果它说得含糊,或者把scale_inv说成和权重在同一个分片,那就是没读准,你可以把get_tensor那段再单独贴一次,让它只解释weight_map[tensor_name]这一行的作用。
验证通过后,再跑本地脚本:
python convert.py \ --input-fp8-hf-path /path/to/deepseek-v3-fp8 \ --output-bf16-hf-path /path/to/deepseek-v3-bf16脚本跑起来后,你会看到 tqdm 进度条逐个分片处理。如果某个权重缺scale_inv,会打印Warning: Missing scale_inv tensor for ..., skipping conversion,这个权重会原样保留,不进fp8_weight_names,它的scale_inv条目也不会被 pop。
跑完后检查输出目录的model.safetensors.index.json,对比输入目录的版本,确认成功转换的权重对应的scale_inv条目已经消失,跳过的权重对应的scale_inv还在。这个对比就是验证索引写回逻辑是否正确的直接证据。
5. 本篇常见错排查
5.1 Codex 请求 404 或路径不对
最常见的原因是 Base URL 多写了/v1。TaoToken 的接入地址是https://taotoken.net/api,不要自己补/v1。如果你填了https://taotoken.net/api/v1,请求路径就会变成/api/v1/...,对不上。改回不带/v1的版本即可。
另一个原因是 Key 没填对,或者 Key 复制时带了空格。重新去控制台复制一次,注意首尾不要有空白字符。
5.2get_tensor抛 KeyError 但文件明明存在
get_tensor里的KeyError来自weight_map[tensor_name],不是load_file。也就是说,weight_map里根本没有这个scale_inv_name这个键。这通常意味着这个权重本身就没有对应的 scale,或者索引文件里 scale 的命名规则和f"{weight_name}_scale_inv"不一致。
排查方法:打开model.safetensors.index.json,搜一下q_proj.weight_scale_inv这个键在不在。如果不在,说明这个权重确实没有 scale,脚本跳过它是预期行为,不是 bug。
5.3 显存不够,load_file到 CUDA 就 OOM
脚本里load_file(file_path, device="cuda")是把整个分片加载到 GPU。loaded_files只保留最近两个分片,但单个分片本身可能就很大。如果你的 GPU 显存不够,可以先把device="cuda"改成device="cpu",转换时再按需搬到 GPU,或者直接用 CPU 做weight_dequant(如果 kernel 支持)。
另一个办法是减小同时保留的分片数,把if len(loaded_files) > 2改成> 1,但这样会增加重复加载的次数,速度会慢。
5.4 转换后索引文件里scale_inv没被删干净
检查fp8_weight_names是不是只收集了成功转换的权重。如果某个权重走了except KeyError分支,它不会进fp8_weight_names,它的scale_inv就不会被 pop。这是设计如此,不是漏删。如果你希望跳过的权重也删掉scale_inv,那要改逻辑,但通常不建议,因为跳过的权重可能还需要 scale 信息。
5.5 Codex 解释的索引流向和实际不符
如果 Codex 说scale_inv和权重在同一个分片,但实际weight_map显示它们在不同文件,那是它没读准get_tensor的逻辑。把get_tensor单独贴出来,强调weight_map[tensor_name]这一行决定了文件,让它重新解释。必要时把model.safetensors.index.json里相关的两个条目也贴给它对照。
6. 读源码这件事,通道通了剩下就是耐心
把 Codex 接到 TaoToken 之后,读 DeepSeek-V3 这份 FP8 转 BF16 脚本的体验会变很多。以前你要手动比对weight_map,现在可以直接把get_tensor、weight_dequant分支、索引写回三段贴给 Codex,让它逐行说索引怎么变,你负责验证它说得对不对。通道本身只是让 Codex 能正常发请求,真正的转换还是本地脚本在跑,TaoToken 不碰张量。
如果你只是临时读这一段,按量用 API 就够;如果后面要长期做源码分析或者 Agent 类任务,可以看下 Coding Plan https://taotoken.net/coding-plan 。接入文档在 https://taotoken.net/doc ,API Keys 在 https://taotoken.net/console/api-keys ,模型对话入口在 https://taotoken.net 。配置改完后,先让 Codex 复述一条权重路径验证通道,再跑本地脚本,顺序别反。