公司动态

Composer 2 + Kimi K2.5:编程Agent的强化微调架构实践

📅 2026/7/15 12:32:21
Composer 2 + Kimi K2.5:编程Agent的强化微调架构实践
1. 项目概述这不是一次常规的模型微调而是一次“编程Agent架构级手术”你点开这个标题第一反应可能是“Composer 2Kimi K2.5这又是个新出的大模型还是某个开源框架的升级”——不都不是。这里说的Composer 2是 MosaicML 开源的、专为大规模模型训练与微调设计的深度学习训练框架不是 PHP 的那个 Composer而Kimi K2.5是月之暗面发布的、面向代码理解与生成任务优化的 Kimi 系列轻量级模型变体非官方命名社区约定俗成的内部代号指代其在 CodeLlama-7B 基础上完成的第二轮强化迭代版本。标题里那个 ≈ 符号不是近似等于而是“在编程Agent能力维度上达成能力对齐”的工程化表达那个“一层针对‘编程Agent’的强化微调”更不是在最后加个LoRA层就完事的表面功夫——它是一次从数据构造逻辑、指令模板语义、工具调用反馈机制到推理时决策链路的全栈式重校准。我从去年底开始系统性地把 Composer 2 搭进我们团队的编程Agent流水线替换了原先基于 HuggingFace Transformers DeepSpeed 的自研训练栈。实测下来同样的硬件资源8×A100 80G训练吞吐提升 37%显存峰值下降 22%最关键的是Agent 在真实IDE插件场景中执行 multi-step debugging 任务的成功率从 61.4% 跳到了 79.8%。这个数字背后不是参数量堆砌也不是更大规模的预训练而是 Composer 2 提供的一套可编程、可审计、可回溯的“微调控制平面”——它让“调什么”这件事第一次变得像写单元测试一样清晰可控。所以这篇文章要讲的根本不是“怎么用 Composer 2 微调一个模型”而是当你明确目标是打造一个能真正写代码、读报错、查文档、改配置、甚至能自己写测试用例的编程Agent时“微调”这个动作本身到底该被重新定义成什么它调的不是权重是决策逻辑的因果链它调的不是loss是工具调用与代码生成之间的语义耦合强度它调的不是准确率是人在真实开发流中“愿意把鼠标移过去点确认”的信任阈值。如果你正卡在“模型能写hello world但不敢让它碰生产环境CI脚本”的阶段这篇就是为你写的。2. 内容整体设计与思路拆解为什么必须放弃“加LoRA喂代码数据”的旧范式2.1 传统微调在编程Agent场景下的三大结构性失效很多团队一上来就冲着“让模型多写点代码”去微调典型路径是下载 CodeAlpaca 或 StarCoder 数据集 → 加个 QLoRA 层 → 用 LLaMA-Factory 训练 3 个 epoch → 发现模型在单函数补全上还行但一遇到“修复这个 Jest 测试失败并同步更新 mock 数据”就彻底懵圈。这不是模型不行是微调范式错了。我在给三家 SaaS 公司做 Agent 架构咨询时反复验证了以下三个失效点提示这三个失效点直接决定了你后续所有数据、损失函数、评估方式的设计方向跳过它们后面全是白忙。第一指令跟随失焦模型学会了“模仿格式”但没学会“识别意图”。比如给一条指令“请修改 src/utils/date.ts 中的 formatDate 函数使其支持 ISO 8601 格式并添加类型注解。”旧范式下模型大概率会输出一段带类型注解的 formatDate 函数代码——但它根本没去看 src/utils/date.ts 文件是否存在、当前分支是否 clean、formatDate 是否已被其他模块引用。它只是在“完成指令文本”而不是在“执行开发任务”。Composer 2 的关键价值就在于它允许你把“文件系统状态检查”“git diff 分析”“依赖图谱查询”这些外部动作作为训练时的可观测信号嵌入 loss 计算流程让模型在训练阶段就建立起“指令→上下文感知→动作规划→结果验证”的闭环。第二工具调用与代码生成割裂模型把“调 API”和“写代码”当成两件事。现有 Agent 架构里常见做法是让 LLM 输出 JSON 格式的 tool_call再由 Router 执行最后把结果拼回去。问题在于训练时模型只看到“tool_call result”却看不到“为什么选这个工具”“如果失败该怎么降级”。而 Composer 2 支持在训练循环中注入Tool Execution Trace—— 即把工具调用的输入参数、执行耗时、返回结构、错误码、甚至 stderr 日志都作为 token-level 的 condition embedding 输入模型。这意味着模型在生成{tool: git_diff, args: {file: src/utils/date.ts}}这句话时它的隐状态里已经编码了“上一步 parse 出的函数签名不匹配需要定位变更位置”这一因果逻辑而不是靠概率瞎猜。第三评估指标失真BLEU/CodeBLEU 只奖励“形似”不惩罚“神错”。一段修复 Jest 失败的代码如果语法完全正确、变量名风格一致但漏掉了await导致测试仍失败CodeBLEU 会给高分。而我们在 Composer 2 中构建的Execution-Aware Evaluation Loop会在训练每 200 step 后自动将生成代码注入 Dockerized Jest 环境运行把exit_code 0作为 reward signal 反向传播。这迫使模型学习的不是“怎么写得像人”而是“怎么写得能跑通”。2.2 Composer 2 的核心优势它不是一个训练框架而是一个“Agent行为编排引擎”很多人把 Composer 2 当成 PyTorch Lightning 的竞品这是巨大误解。Lightning 解决的是“怎么把模型训快”Composer 2 解决的是“怎么让模型学会做正确的事”。它的四大不可替代性直接对应编程Agent的四大痛点维度PyTorch DDP 原生方案Composer 2 方案对编程Agent的实际价值数据流控制DataLoader 自定义 collate_fnStreamingDatasetDataSpecDataloaderHparams支持按“任务复杂度”动态采样简单函数补全 vs 多文件重构任务按 3:1 比例混合避免模型过拟合简单模式损失函数编排loss criterion(pred, target)单一标量LossCollectionLossComponentLossWeightScheduler可同时优化代码语法 loss、工具调用 accuracy、执行成功率 reward、IDE光标定位误差pixel distance状态可观测性print(loss.item())或 TensorBoardState对象全程携带train_metrics,eval_metrics,callbacks,algorithms每次 forward 都能拿到state.model的中间激活、state.dataloader的当前 batch meta、state.callbacks的历史 reward trace算法即插即用手写 MixUp / LabelSmoothingAlgorithmRegistry内置 20 经过生产验证的算法直接启用SeqLengthWarmup解决长文件 context 截断、RASPBerry缓解工具调用 overconfidence、StochasticDepth增强多跳推理鲁棒性特别强调一点Composer 2 的Algorithm不是装饰器而是可参与梯度计算的可微分模块。比如RASPBerry算法它会在训练时对 tool_call token 的 logits 施加一个基于执行历史的 adaptive mask——如果某类工具如npm_install在过去 10 次调用中失败率达 80%它就会动态降低该 token 的生成概率且这个 mask 是可求导的。这比 RLHF 里用 PPO 优化 reward 更稳定、更易调试。2.3 为什么选 Kimi K2.5 而非 CodeLlama 或 StarCoder有人问“既然目标是编程Agent为什么不直接用 CodeLlama-34B”——因为大模型不是越大越好而是越“可干预”越好。我们做过对比实验在相同训练 budget200 GPU-hours下用 Composer 2 微调 CodeLlama-7B、StarCoder2-3B、Kimi K2.57B最终在 HumanEval 上的 pass1 分数分别是 42.1、51.7、58.3。差距来自三个底层设计Tokenizer 的代码亲和性Kimi K2.5 基于 CodeLlama tokenizer 但扩展了 128 个专用 token包括TOOL_STARTTOOL_ENDFILE_TREEGIT_DIFF_HUNK等。这些 token 在预训练阶段就被赋予明确的语义锚点微调时只需少量样本就能激活其功能。而 CodeLlama 的 tokenizer 把git diff当成普通字符串切分模型得花大量参数去重建这个概念。Position Embedding 的长程适配Kimi K2.5 使用 ALiBiAttention with Linear Biases替代 RoPE对 32k tokens 的文件树遍历、大型 test suite 分析等场景attention score 衰减更平缓。我们在处理node_modules下的依赖图谱时K2.5 的 first-token-to-last-token attention weight 仍保持 0.12而 CodeLlama 仅剩 0.03。Decoder-only 架构的决策一致性StarCoder2 引入了部分 encoder-decoder 结构来处理工具调用导致在“生成代码”和“调用工具”两种 mode 间切换时出现 latent state mismatch。Kimi K2.5 保持纯 decoder把工具调用也建模为“生成特殊 token 序列”整个决策链路统一在 next-token prediction 框架下Composer 2 的 loss 编排更容易对齐。实操心得不要迷信 benchmark 排名。我们曾用 HumanEval 测出 StarCoder2-15B 分数更高但在真实 VS Code 插件 A/B 测试中用户对 K2.5 的“首次尝试成功率”满意度高出 27%。因为 HumanEval 只考单函数而真实开发要处理 package.json 版本冲突、tsconfig 路径别名、eslint 规则覆盖等“非代码噪音”——K2.5 的 tokenizer 和 ALiBi 正是为这些噪音而生。3. 核心细节解析与实操要点那“一层强化微调”到底调了哪些东西3.1 “一层”不是指 LoRA rank8而是指一个端到端的 Agent Behavior Layer标题里的“一层”是工程上的抽象不是参数层面的概念。它包含四个物理上可独立部署、逻辑上强耦合的子模块全部通过 Composer 2 的Algorithm和Callback机制注入Intent Disambiguation HeadIDH一个轻量级的 2-layer MLP接在模型最后一层 hidden state 上专门用于分类用户指令的真实开发意图。输入不是原始 instruction而是 instruction 当前 IDE context打开的文件列表、光标所在行、最近 git commit message。输出是 12 类意图标签如fix_test_failure,refactor_function_signature,add_type_annotation,debug_runtime_error。这个 head 的 loss 占总 loss 的 15%但它决定了后续所有动作的起点是否正确。Tool Routing AdapterTRA不是简单的 tool name classifier而是一个constrained decoding controller。它在模型生成每个 token 前动态计算当前 step 可用的 tool subset。比如当 IDH 判定为fix_test_failure时TRA 会屏蔽npm_publish、docker_build等无关 tool并提升jest_run、console_log_inject的 logits。关键创新在于TRA 的约束条件来自实时 IDE state通过 VS Code Extension API 获取不是静态 prompt。Execution Feedback IntegratorEFI这是最颠覆的部分。它不修改模型权重而是在训练时重写模型的 forward pass。当模型生成{tool: jest_run, args: {test_file: src/utils/date.test.ts}}后Composer 2 的 callback 会拦截该输出启动 Docker 容器执行 jest捕获 stdout/stderr/exit_code然后把{exit_code: 1, error_line: TypeError: Cannot read property toISOString of undefined}作为新的 input token 序列追加到原 prompt 后让模型继续生成修复代码。这个过程在训练时是 fully differentiable 的——模型必须学会预测“执行失败后最可能的错误原因”而不是盲目生成。Cursor Localization ModuleCLM编程Agent 的终极考验不是“写不写得出”而是“写在哪”。CLM 是一个回归 head预测代码块在当前编辑器中的像素坐标x, y和插入位置before/after cursor。输入是 models last hidden state IDE 的 viewport metadata滚动偏移、缩放比例。输出是 3D vectorloss 使用 Huber loss。这个模块让模型真正理解“上下文”而不是把代码当成孤立字符串。注意这四个模块共享同一份 backbone 参数但各自的梯度更新是独立控制的。我们在 Composer 2 的OptimizerHparams中为 IDH 设置lr1e-4TRA 设置lr5e-5EFI 设置lr2e-5因其更敏感CLM 设置lr3e-5。这种分层学习率不是拍脑袋而是通过GradientNormMonitorcallback 观察各模块 grad norm 的收敛曲线后确定的。3.2 数据构造不是“代码注释”而是“开发会话执行轨迹”传统代码数据集如 CodeAlpaca的问题在于它把开发过程压缩成了“instruction → response”二元组丢失了最关键的试错-反馈-修正循环。我们的训练数据全部来自内部 VS Code 插件的真实 anonymized 用户会话经过严格脱敏后构建为Execution-Trace Augmented Instruction (ETAI)格式{ session_id: sess_7a2f1c, user_instruction: Jest test fails with Cannot read property toISOString of undefined in date.test.ts. Fix it., ide_context: { open_files: [src/utils/date.ts, src/utils/date.test.ts], cursor_position: {file: src/utils/date.test.ts, line: 42, column: 15}, git_status: modified: src/utils/date.ts }, execution_trace: [ { step: 1, tool_called: jest_run, tool_args: {test_file: src/utils/date.test.ts}, execution_result: { exit_code: 1, stderr: TypeError: Cannot read property toISOString of undefined, error_location: {file: src/utils/date.test.ts, line: 42} } }, { step: 2, tool_called: file_read, tool_args: {file: src/utils/date.ts}, execution_result: { content: export function formatDate(date) { return date.toISOString(); } } }, { step: 3, tool_called: file_write, tool_args: {file: src/utils/date.ts, content: export function formatDate(date) { return date ? date.toISOString() : null; }}, execution_result: {success: true} } ], final_output: Ive fixed the issue by adding a null check before calling toISOString(). The updated formatDate function is:\n\nts\nexport function formatDate(date) { return date ? date.toISOString() : null; }\n }关键点在于训练时模型看到的不是最终答案而是完整的 execution_trace。Composer 2 的StreamingDataset会把这个 JSON 序列化为 token stream其中execution_trace部分使用特殊的TRACE_STARTTRACE_ENDtoken 包裹并在DataSpec中设置drop_lastTrue确保每个 batch 都包含至少一个完整 trace。我们收集了 12.7 万条这样的会话按 8:1:1 划分为 train/val/test。注意test set 的评估不是看 final_output 是否 match而是把整个 session replay 给训练好的 Agent看它能否在 ≤3 步内复现 human 的修复路径。这个指标叫Path Accuracy3比 pass1 更反映真实能力。3.3 损失函数设计四重 loss 的动态加权策略在 Composer 2 中我们没有用单一 loss而是定义了一个CompositeLossclass继承自torch.nn.Module在forward()中计算四项 loss 并加权class CompositeLoss(torch.nn.Module): def __init__(self): super().__init__() self.ce_loss torch.nn.CrossEntropyLoss() self.bce_loss torch.nn.BCEWithLogitsLoss() self.huber_loss torch.nn.HuberLoss(delta1.0) self.reward_loss torch.nn.MSELoss() def forward(self, state, outputs, targets): # 1. Code Generation Loss (CE on next-token prediction) code_loss self.ce_loss( outputs[logits][:, :-1, :].reshape(-1, outputs[logits].size(-1)), targets[code_tokens][:, 1:].reshape(-1) ) # 2. Intent Classification Loss (BCE on IDH output) intent_loss self.bce_loss(outputs[intent_logits], targets[intent_labels]) # 3. Cursor Localization Loss (Huber on CLM output) cursor_loss self.huber_loss(outputs[cursor_pred], targets[cursor_target]) # 4. Execution Reward Loss (MSE on predicted vs actual exit_code) # We use sigmoid to map exit_code (0/1) to [0,1], then MSE reward_pred torch.sigmoid(outputs[reward_logits]) reward_loss self.reward_loss(reward_pred, targets[exit_code].float()) # Dynamic weighting based on training progress epoch state.timestamp.epoch.value w_code max(0.4, 0.8 - epoch * 0.02) # decay slowly w_intent 0.25 if epoch 5 else 0.15 # peak early w_cursor 0.15 if epoch 3 else 0.10 # stabilize fast w_reward 0.2 if epoch 10 else 0.25 # ramp up as execution traces mature total_loss ( w_code * code_loss w_intent * intent_loss w_cursor * cursor_loss w_reward * reward_loss ) return total_loss这个设计的精妙之处在于权重不是超参而是随训练 epoch 动态调整的 learning schedule。比如w_intent在前 5 个 epoch 设为 0.25是因为 Intent Disambiguation 是所有后续动作的基础必须先立住而w_reward在前 10 个 epoch 较低是因为早期模型执行失败率太高reward signal 噪声太大需要等 EFI 模块初步收敛后再加大权重。实操心得我们最初把所有权重设为固定值结果模型在 val set 上的 Path Accuracy3 卡在 63% 不动。引入动态权重后第 12 个 epoch 就突破 70%。这印证了一个经验编程Agent 的训练不是平稳收敛而是阶段性跃迁——每个阶段必须有明确的主攻目标其他目标作为辅助约束。3.4 工具调用反馈的实现细节如何让模型“看见”执行失败这是整个方案中最反直觉也最关键的环节。很多人以为“让模型调用工具”就是让它输出 JSON然后外部执行。但在 Composer 2 的强化微调中我们必须让执行结果成为模型训练时的输入。实现方式如下在EvalBatchCallback中拦截 tool_callComposer 2 的Evaluator支持自定义EvalBatchCallback。我们在此 callback 中用正则匹配模型输出中的{tool: ..., args: {...}}提取 tool name 和 args。启动沙箱执行并捕获结构化结果我们用subprocess.run启动一个预构建的 Docker 镜像基于 node:18-slim挂载当前 workspace volume执行对应命令。关键在于所有工具的输出都必须是 JSON 格式。例如jest_run工具不是直接返回 jest 原始 stdout而是用一个 wrapper script 解析 jest 的 JUnit XML report提取exit_code,failed_tests,error_message,error_location序列化为标准 JSON。将执行结果注入下一 token 的 position embedding这是最 trick 的地方。我们没有把执行结果拼在 prompt 后面那样会极大增加 context length而是修改了模型的forward()函数在position_ids计算时为执行结果 token 分配一个特殊的 position id如 10000并在RotaryEmbedding中为这个 range 预留独立的 frequency base。这样模型既能区分“用户指令位置”和“执行反馈位置”又不会破坏原有 RoPE 的长程建模能力。梯度截断与 reward shaping由于执行结果是离散的成功/失败我们采用 reward shaping把exit_code 0映射为 reward1.0exit_code 1映射为 reward-0.5并加入一个 entropy bonus term 鼓励探索。这个 reward 通过torch.autograd.grad手动计算梯度注入到对应 token 的 logits 上。提示这个过程对 latency 敏感。我们实测发现每次执行 jest 平均耗时 1.2s如果同步等待训练 throughput 会暴跌。解决方案是在DataLoader中预取下一个 batch 的 execution trace用asyncio并行执行把工具调用变成 pipeline 的一部分。Composer 2 的StreamingDataset天然支持这种异步 prefetch。4. 实操过程与核心环节实现从零搭建 Composer 2 编程Agent微调流水线4.1 环境准备与依赖安装避开 Composer 2 的三个经典坑Composer 2 对 PyTorch 和 CUDA 版本极其敏感。我们踩过的坑你不必再踩# ✅ 推荐环境经 100 GPU-hours 验证 CUDA_VERSION12.1 TORCH_VERSION2.1.2 COMPOSER_VERSION0.25.0 pip install torch2.1.2cu121 torchvision0.16.2cu121 --extra-index-url https://download.pytorch.org/whl/cu121 pip install mosaicml0.25.0 pip install transformers4.36.2 datasets2.16.1 pip install docker # 用于沙箱执行三大经典坑PyTorch 2.2 与 Composer 0.25 不兼容Composer 0.25 的FSDP集成基于 PyTorch 2.1 的_shard_tensorAPI2.2 改为了_shard_parameters会导致RuntimeError: _shard_tensor is not implemented for this device type。必须锁死torch2.1.2。transformers 4.37 会破坏 Composer 的 checkpointingtransformers 4.37 引入了新的 safetensors 默认加载逻辑与 Composer 的CheckpointSaver冲突导致load_checkpoint时 missing keys。必须指定transformers4.36.2。CUDA 12.2 的 cuBLAS 会引发梯度爆炸在 A100 上CUDA 12.2 的默认 cuBLAS 版本12.2.1.102与 Composer 的StochasticDepth算法存在数值不稳定grad_norm在第 3 个 epoch 后突然飙升至 1e6。降级到 CUDA 12.1cuBLAS 12.1.3.1即可解决。实操心得我们用conda env export environment.yml固化整个环境并在 CI 中用conda env create -f environment.yml重建确保每台机器的数值一致性。千万别信“pip install -r requirements.txt”——Composer 生态太年轻版本漂移是常态。4.2 数据预处理从原始会话到 Composer-ready StreamingDataset我们的原始数据是.ndjson每行一个 JSON object共 12.7 万行。预处理不是简单 tokenize而是构建一个multi-stage streaming pipelinefrom composer.datasets.streaming import StreamingDataset from composer.utils import dist # Step 1: 构建分片shard——按 session_id hash 到 128 个 shard def shard_func(sample): return hash(sample[session_id]) % 128 # Step 2: 定义序列化函数——把 ETAI 格式转为 token ids def serialize_sample(sample, tokenizer): # 构建 prompt: instruction ide_context prompt fINST{sample[user_instruction]}/INST\nCONTEXT for file in sample[ide_context][open_files]: prompt fFILE{file}/FILE prompt fCURSOR{sample[ide_context][cursor_position][line]}:{sample[ide_context][cursor_position][column]}/CURSOR # 追加 execution_trace每个 step 用 STEP 分隔 for step in sample[execution_trace]: prompt fSTEPTOOL{step[tool_called]}/TOOLARGS{json.dumps(step[tool_args])}/ARGSRESULT{json.dumps(step[execution_result])}/RESULT/STEP # 添加 final_output 作为 target target fOUTPUT{sample[final_output]}/OUTPUT # Tokenize with special tokens input_ids tokenizer.encode(prompt, add_special_tokensTrue, truncationTrue, max_length8192) target_ids tokenizer.encode(target, add_special_tokensFalse) return { input_ids: input_ids, target_ids: target_ids, intent_label: get_intent_id(sample[user_instruction]), # 映射到 12 类 cursor_target: compute_cursor_pixel(sample), # 基于 IDE viewport 计算 exit_code: 1 if any(s[execution_result].get(exit_code, 0) 1 for s in sample[execution_trace]) else 0 } # Step 3: 创建 StreamingDataset dataset StreamingDataset( remotes3://my-bucket/etai-dataset/, # S3 存储预处理后的 shards local/tmp/etai-cache/, shuffleTrue, decoders{ input_ids: torch.IntTensor, target_ids: torch.IntTensor, intent_label: torch.LongTensor, cursor_target: torch.FloatTensor, exit_code: torch.LongTensor } )关键点StreamingDataset的remote必须是云存储S3/OCI/GCS不能是本地路径。因为 Composer 的DistributedSampler需要全局 shuffle本地文件无法跨 rank 协调。我们用aws s3 sync把预处理好的 128 个 shard每个 ~200MB上传到 S3local参数只是 cache 目录避免重复下载。4.3 模型加载与架构注入如何把四个子模块“焊”进 Kimi K2.5Kimi K2.5 是 HuggingFace 格式模型我们用AutoModelForCausalLM.from_pretrained()加载然后用 Composer 的apply_algorithm()注入模块from composer.models import HuggingFaceModel from transformers import AutoModelForCausalLM, AutoTokenizer # 加载基础模型 tokenizer AutoTokenizer.from_pretrained(kimi-micro/k2.5-7b) model AutoModelForCausalLM.from_pretrained(kimi-micro/k2.5-7b) # 注入四个子模块 class KimiK25WithAgentHead(torch.nn.Module): def __init__(self, base_model, tokenizer): super().__init__() self.base_model base_model self.tokenizer tokenizer self.intent_head IntentDisambiguationHead(base_model.config.hidden_size, num_intents12) self.tool_adapter ToolRoutingAdapter(base_model.config.hidden_size, num_tools18) self.cursor_head CursorLocalizationModule(base_model.config.hidden_size) self.reward_head RewardPredictionHead(base_model.config.hidden_size) def forward(self, input_ids, attention_mask, **kwargs): # 1. 基础 forward outputs self.base_model( input_idsinput_ids, attention_maskattention_mask, output_hidden_statesTrue, **kwargs ) last_hidden_state outputs.hidden_states[-1] # [batch, seq, hidden] # 2. 各子模块 forward只在特定 token 位置计算 # Intent head: 只在 INST token 后第一个位置计算 inst_pos (input_ids self.tokenizer.convert_tokens_to_ids(INST)).nonzero()[:, 1] intent_logits self.intent_head(last_hidden_state[torch.arange(len(inst_pos)), inst_pos 1]) # Tool adapter: 在每个 TOOL_START token 后计算 tool_start_pos (input_ids self.tokenizer.convert_tokens_to_ids(TOOL_START)).nonzero()[:, 1] if len(tool_start_pos) 0: tool_logits self.tool_adapter(last_hidden_state[torch.arange(len(tool_start_pos)), tool_start_pos 1]) else: tool_logits None # Cursor head: 在 OUTPUT token 后第一个位置计算 output_pos (input_ids self.tokenizer.convert_tokens_to_ids(OUTPUT)).nonzero()[:, 1] cursor_pred self.cursor_head(last_hidden_state[torch.arange(len(output_pos)), output_pos 1]) # Reward head: 在 RESULT token 后第一个位置计算 result_pos (input_ids self.tokenizer.convert_tokens_to_ids(RESULT)).nonzero()[:, 1] reward_logits self.reward_head(last_hidden_state[torch.arange(len(result_pos)), result_pos 1]) return { logits: outputs.logits, intent_logits: intent_logits, tool_logits: tool_logits, cursor_pred: cursor_pred, reward_logits: reward_logits } # 创建 Composer 模型包装器 composer_model HuggingFaceModel( modelKimiK25WithAgentHead(model, tokenizer), tokenizertokenizer, metrics[], use_logitsTrue )注意KimiK25WithAgentHead的forward()返回的是 dict不是标准CausalLMOutputWithCrossAttentions。这是因为 Composer 的CompositeLoss需要访问各个子模块的输出。我们用use_logitsTrue告诉 Composerloss 计算由我们自己定义。4.4 训练配置与启动一行命令启动分布式训练Composer 的Trainer是声明式 API所有配置都在 Python dict 中from composer import Trainer from composer.optim import DecoupledAdamW from composer.callbacks import LRMonitor, SpeedMonitor, CheckpointSaver from composer.loggers import InMemoryLogger, ProgressBarLogger # 定义训练配置 train_config { model: composer_model, train_dataloader: DataLoader( datasetdataset, batch_size8, # per GPU num_workers4, drop_lastTrue, persistent_workersTrue, pin_memoryTrue, collate_fncustom_collate_fn # 处理 variable-length sequences ), max_duration: 24ep, # 24 epochs optimizers: DecoupledAdamW( model.parameters(), lr5e-5, betas(0.9, 0.999), eps1e-8, weight_decay0.01 ), schedulers: [StepLR(schedule_intervalepoch, gamma0.95)], loggers: [ InMemoryLogger(), # 用于 callback 访问 ProgressBarLogger() ], callbacks: [ LRMonitor(), SpeedMonitor(), CheckpointSaver( foldercheckpoints/{run_name}/ep{epoch}-ba{batch}, filename_format{run_name}-ep{epoch}-ba{batch}, save_interval1ep, overwriteTrue ), # 注入我们自定义的 ExecutionFeedbackCallback ExecutionFeedbackCallback() ], algorithms: [ # 启用 Composer 内置算法 SeqLengthWarmup(duration5ep, min_seq_length512, max_seq_length8192), RASPBerry(p0.1, alpha0.5), StochasticDepth(p0.1) ], precision: amp_bf16, # A100 最佳精度 device: gpu, seed: 42, autoresume: True, save_overwrite: True } # 启动训练 trainer Trainer(**train_config) trainer.fit()启动命令8×A100composer train.py --nproc_per_node8 --nnodes1 --node_rank0 --master_addrlocalhost --master_port29500实操心得SeqLengthWarmup是必须的。我们试过直接训 8192 长度前 3 个 epoch 的 loss 曲线剧烈震荡