
1. 为什么单机 A100 复现 DeepSeek R1 总卡在“环境与通道”这两关DeepSeek R1 本地训练这件事真正动手过的人都知道卡点从来不在“算法看不懂”而在两件很琐碎的事一是 open-r1 这套训练框架对 CUDA、vLLM、DeepSpeed 的版本咬得很死二是训练脚本里散落着各种模型下载、数据拉取、评测调用的 API 通道Key 一多就乱报错还特别隐蔽。这篇面向的是手里有一台 8 卡 A10040G、想用 open-r1 复现 DeepSeek R1 训练流程、基座选 Qwen-14B 的开发者。核心检索词就是 DeepSeek R1 本地训练、open-r1、Qwen-14B、A100。我会把整条链路拆成可复制的步骤从统一 Key 的配置到 open-r1 的启动命令再到 Qwen-14B 权重加载参数、loss 曲线和显存占用的验证动作。很多人以为照着 open-r1 的 README 跑就行结果第一步pip install就撞上 flash-attn 编译失败第二步 vLLM 推理 OOM第三步 reward 函数形参名和数据集列名对不上训练直接静默返回全 0 奖励。更麻烦的是训练过程中要调用外部模型做数据合成、做评测、做 reward 校验时每个工具一套 Key、一套 Base URL配置散落在.env、config.yaml、auth.json里改一处忘一处。我试过把模型下载、数据合成、评测这几个环节的通道统一到一个入口用 TaoToken 的 Key 做统一鉴权训练脚本里只维护一份配置后面排障时定位问题快很多。下面按“先打通通道再跑通训练最后验证结果”的顺序来写每一步都给可复制的片段。2. TaoToken 统一 Key 前置把 open-r1 里分散的 API 通道收拢到一处open-r1 的训练流程里其实不止“训练”一个动作会碰网络。典型的有三类第一类是拉取基座模型权重和数据集HuggingFace 通道第二类是训练中调用外部模型做数据合成或蒸馏比如生成 reasoning 轨迹第三类是训练后的评测与 benchmark。这三类如果各用各的 Key配置文件会非常碎。TaoToken 在这里的角色是一个统一的 API 入口把模型对话、coding 相关的调用收敛到同一个 Base URL 和同一把 Key 上。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 这个不加 UTM。你需要在控制台里创建 Key控制台入口在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。创建完 Key 之后先别急着往训练脚本里塞。建议在项目根目录建一个.env把统一通道写进去训练脚本和评测脚本都读这一份# .env 放在 open-r1 项目根目录 TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的统一Key TAOTOKEN_MODEL_IDdeepseek-r1这里TAOTOKEN_MODEL_ID是给数据合成、评测这类调用用的模型标识。训练本身用的是本地 Qwen-14B 权重不走这个通道但训练前后的辅助环节会用到。这样区分开就不会出现“训练脚本误把远程模型当基座”的低级错误。如果你用的是 Claude Code 这类编码工具来辅助改 open-r1 的代码它的配置也是同一套三件套Base URL、Key、Model ID。配置片段长这样{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的统一Key, ANTHROPIC_MODEL: deepseek-r1 } }注意这里 Base URL 用的是https://taotoken.net/api不要带 UTM 参数否则某些客户端会把它当成非法路径。Key 只在控制台生成一次复制后妥善保存页面上不会二次明文展示。把通道收拢之后open-r1 里那些原本写死 HuggingFace endpoint 或各家 API 的地方就可以改成读环境变量。比如数据合成脚本里原本可能是client OpenAI(base_urlhttps://某家地址/v1, api_key某把旧Key)改成import os from openai import OpenAI client OpenAI( base_urlos.environ[TAOTOKEN_BASE_URL], api_keyos.environ[TAOTOKEN_API_KEY], )这样换环境、换 Key 只改.env一处。实测下来这一步能省掉后面大量“为什么这个脚本报 401、那个脚本却正常”的排查时间。统一通道不是为了炫技是为了让排障时变量更少。3. 可复制配置open-r1 环境、Qwen-14B 权重加载与 GRPO 启动参数这一节是全文的技术核心给的是能直接抄的配置。先说环境。open-r1 要求 CUDA 12.4显卡驱动 470 以上基本能跑我用的是 535。先验证 torch 能不能看到卡import torch print(torch.cuda.is_available()) # 期望 True print(torch.cuda.device_count()) # 期望 8环境用 conda 管理比 uv 更符合大多数人的习惯conda create -n openr1 python3.11 -y conda activate openr1 pip install torch2.5.1 torchvision0.20.1 torchaudio2.5.1 \ --index-url https://download.pytorch.org/whl/cu124 pip install vllm0.7.2 pip install flash-attn --no-build-isolation cd open-r1 pip install -e .[dev]flash-attn一定要加--no-build-isolation否则它会去拉一个和当前 torch 不匹配的版本编译到一半报错。这是第一个高频坑。接下来是 Qwen-14B 的权重加载参数。在recipes/Qwen2.5-14B-Instruct/grpo/config_simple_rl.yaml里模型相关部分这样写# Model arguments model_name_or_path: /workspace/models/Qwen2.5-14B-Instruct model_revision: main torch_dtype: bfloat16 attn_implementation: flash_attention_2torch_dtype用bfloat16A100 原生支持比 fp16 稳。attn_implementation用flash_attention_2显存和速度都更好。权重路径建议用绝对路径避免 accelerate 在子进程里解析相对路径出错。GRPO 训练的关键参数8 卡 A100 里 7 卡训练、1 卡推理# GRPO trainer config num_processes: 7 use_vllm: true vllm_device: cuda:7 vllm_gpu_memory_utilization: 0.2 vllm_max_model_len: 8000 bf16: true gradient_accumulation_steps: 8 gradient_checkpointing: true gradient_checkpointing_kwargs: use_reentrant: false learning_rate: 3.0e-06 lr_scheduler_type: cosine max_prompt_length: 512 max_completion_length: 1024 num_generations: 7 per_device_train_batch_size: 8 per_device_eval_batch_size: 8 logging_steps: 5 save_steps: 100 save_total_limit: 2 output_dir: /workspace/Qwen-14B-Instruct-RL report_to: nonevllm_gpu_memory_utilization: 0.2是 14B 模型在 40G 卡上的关键值7B 可以放到 0.5。vllm_max_model_len: 8000控制 KV cache 占用默认会读基座 config 的 32768直接 OOM。这两个参数是推理侧 OOM 的解药。DeepSpeed 的 zero3 配置要开 offload否则 7 卡扛不住 14B 的训练态# recipes/accelerate_configs/zero3.yaml compute_environment: LOCAL_MACHINE distributed_type: DEEPSPEED mixed_precision: bf16 num_machines: 1 num_processes: 8 deepspeed_config: zero_stage: 3 offload_optimizer_device: cpu offload_param_device: cpu zero3_init_flag: true zero3_save_16bit_model: true deepspeed_multinode_launcher: standard use_cpu: false启动命令ACCELERATE_LOG_LEVELinfo accelerate launch \ --config_file recipes/accelerate_configs/zero3.yaml \ --num_processes7 src/open_r1/grpo.py \ --config recipes/Qwen2.5-14B-Instruct/grpo/config_simple_rl.yaml \ /workspace/Qwen2.5-14B-Instruct.log注意--num_processes7和 yaml 里的num_processes: 7要一致第 8 张卡留给 vLLM。启动后日志会先打印模型加载进度再进入训练循环。4. 验证请求与成功结果loss 曲线、显存占用与一次真实调用训练跑起来之后怎么判断它是真的在学而不是在空转看三个东西日志里的 loss、nvidia-smi的显存、以及一次真实的通道调用。先看 loss。GRPO 的日志里会有reward和loss两类指标。健康的曲线是 reward 缓慢上升、loss 在合理区间波动。如果 reward 一直是 0八成是 reward 函数形参名和数据集列名没对齐。比如数据集列叫solutionreward 函数形参却写成ground_truth那取到的永远是空值奖励恒为 0。正确写法def accuracy_reward_ours(completions, solution, **kwargs): contents [c[0][content] for c in completions] rewards [] for content, sol in zip(contents, solution): answer re.findall(ranswer(.*?)/answer, content) if answer: rewards.append(1.0 if answer[0] sol else 0.0) else: rewards.append(0.0) return rewards形参solution必须和data.json里的列名一致。数据集格式{problem: Classify the text into neutral, negative, or positive\nText: I think the food was okay.\nSentiment:\n, solution: positive} {problem: Classify the text into neutral, negative, or positive\nText: I think the food was shit.\nSentiment:\n, solution: negative}再看显存。训练时开一个终端跑watch -n 2 nvidia-smi正常状态是 0-6 号卡显存占用较高训练态 offload7 号卡占用较低vLLM 推理。如果 7 号卡爆满然后进程被杀就是推理侧 OOM回去调vllm_gpu_memory_utilization和vllm_max_model_len。如果 0-6 号卡爆满是训练侧 OOM检查 zero3 的 offload 有没有生效。最后验证统一通道。写一个小脚本确认 Key 和 Base URL 能通import os from openai import OpenAI client OpenAI( base_urlos.environ[TAOTOKEN_BASE_URL], api_keyos.environ[TAOTOKEN_API_KEY], ) resp client.chat.completions.create( modelos.environ[TAOTOKEN_MODEL_ID], messages[{role: user, content: 用一句话说明什么是 GRPO}], ) print(resp.choices[0].message.content)能正常打印出内容说明通道没问题。这一步在训练前做一次能避免训练中途因为通道问题中断。模型对话入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 可以在这里先手动验证模型可用性。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 逐条对照排障这节按真实报错来每条给现象、原因、动作。401 Unauthorized。现象是调用返回 401。原因通常是 Key 没读到、Key 失效、或者 Base URL 写错。检查.env有没有被 sourceecho $TAOTOKEN_API_KEY看有没有值。Base URL 必须是https://taotoken.net/api多一个斜杠或少一个/api都会 401。如果用的是 Claude Code 类工具检查ANTHROPIC_BASE_URL是否指向同一地址。local proxy failed。现象是客户端报本地代理失败。这通常是客户端配置里残留了旧的代理设置或者环境变量HTTP_PROXY/HTTPS_PROXY指向了一个不可用的地址。检查env | grep -i proxy把无关的代理变量清掉让请求直连统一入口。reading choices 报错。现象是解析响应时resp.choices为空或报reading choices。原因一般是返回体不是标准 chat completion 格式可能是模型 ID 写错导致返回了错误结构或者请求被中间层拦截返回了 HTML。先打印完整resp看结构确认TAOTOKEN_MODEL_ID是有效模型标识。OAuth 相关报错。现象是提示 OAuth 认证失败或 token 过期。这类多出现在用 OAuth 流程的工具里。解决方式是改用 API Key 直连把ANTHROPIC_API_KEY或对应的 Key 字段填成控制台生成的 Key不要走 OAuth 交互流程。Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 重新生成一把再试。训练侧 OOM。现象是进程被 kill日志里有 CUDA out of memory。先看报错的卡号。GPU 7 报错是推理侧调vllm_gpu_memory_utilization到 0.2、vllm_max_model_len到 8000。GPU 0-6 报错是训练侧确认 zero3.yaml 里offload_optimizer_device和offload_param_device都是cpuzero_stage是 3。reward 恒为 0。现象是日志里 reward 一直是 0.0。检查 reward 函数形参名和数据集列名是否一致检查模型输出里有没有answer标签检查max_completion_length是不是太短导致答案被截断。flash-attn 编译失败。现象是pip install flash-attn报编译错误。加--no-build-isolation并确认 torch 已经装好且版本匹配。如果还失败检查 CUDA toolkit 版本是不是 12.4。权重加载报 key 不匹配。现象是加载 Qwen-14B 时提示 missing keys 或 unexpected keys。确认model_name_or_path指向的是完整的 Qwen2.5-14B-Instruct 目录里面有config.json、model.safetensors等文件。路径不要指向空目录或只有部分文件的目录。排障的核心思路是先定位是通道问题还是训练问题再看报错的卡号和阶段。通道问题看 401 和 proxy训练问题看 OOM 和 reward。把变量控制住问题就好找。6. 长期训练与 Agent 场景把统一通道固化进你的工作流单次跑通只是开始。如果你打算长期做 DeepSeek R1 的本地训练和迭代建议把统一通道固化进工作流而不是每次手动配。具体做法有三点。第一把.env纳入版本管理时用.env.example模板真实 Key 不进仓库。模板里写清楚三个变量名团队成员各自填自己的 Key。这样换人、换机器只改本地文件。第二训练脚本、数据合成脚本、评测脚本统一读同一份环境变量。不要在一个脚本里写死 Base URL在另一个脚本里写死 Key。统一入口的价值就在于“改一处全生效”。第三如果训练流程里涉及 Agent 式的多步调用比如让模型自己生成 reasoning 轨迹再筛选这类长链路任务对通道稳定性要求更高。可以考虑用 Coding Plan 这类面向长期编码和 Agent 场景的方案入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 把调用配额和通道管理交给它你专注在训练逻辑上。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言 SDK 的接入示例改 open-r1 里的调用时可以直接参考。Claude Code 相关的接入说明在 https://taotoken.net/claudecode?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 如果你用 Claude Code 辅助改训练代码按那里的三件套配好即可。最后给一个实操建议每次改完配置先跑一次第 4 节里的通道验证脚本再启动训练。这个习惯能帮你把“通道问题”和“训练问题”彻底分开排障时间至少省一半。训练日志建议重定向到文件并加时间戳方便回溯 loss 曲线的拐点对应哪次参数调整。Qwen-14B 在 8 卡 A100 上跑 GRPO第一轮 epoch 的 loss 下降通常在前 100 步内就能看到如果 200 步还没动静回去查 reward 函数和数据集对齐。