AMD Qwen3.5 训练的故障定位与性能优化
在适配基于 LLaMA-Factory 的多平台训练运行时时,AMD R9700(gfx1201)上先后遇到了两个问题:视觉 patch embedding 走到异常慢的卷积路径,以及 Qwen3.5 训练早期触发 GPU memory access fault。前者影响吞吐,后者使训练无法完成,需要分别定位。
Qwen3-VL 在视觉层等价改写后可以运行,但 Qwen3.5-2B 的纯文本训练仍常在第 2~3 步出错,多模态训练则曾在第 8 步触发 fault。最终通过旧栈回退、新用户态栈重新启用 hipBLASLt 的对照,恢复了稳定训练,并将同配置 100 步测试的平均耗时从 23.77 秒降到 6.20 秒。
一、先把训练输入与环境固定下来
平台化训练增加了容器、任务配置、数据预处理和模型加载等环节。出现故障时,需要保留最终生效的训练配置,而不是只记录界面上的参数;同样也要确认容器实际看到的 GPU、PyTorch/HIP 版本及库路径。
本次对照使用同一台主机、同一份 tokenized 数据和同一套训练参数,宿主驱动保持不变。多模态 batch 中确认存在真实的 pixel_values 与视觉网格信息,再用不含视觉输入的文本路径缩小范围。固定预处理结果可以避免每次重试时图片数量、序列长度和截断位置发生变化。
一次优化器更新不一定只有一次前反向。这里采用每设备 batch=1、梯度累积 8、BF16、LoRA 和梯度检查点;一个训练 step 内可能经过多个微批次及重计算。只测一个普通 BF16 GEMM,远不能覆盖真实训练的全部形状与执行顺序。
二、用对照排除容易混淆的原因
| 对照 | 观测结果 | 对定位的意义 |
|---|---|---|
| 不含视觉输入的文本训练 | 旧栈仍可复现 fault | 视觉塔不是唯一触发来源 |
| 视觉 Conv3D 改写为矩阵乘法 | Qwen3.5 文本路径仍失败 | 性能补丁与本次 fault 需要分开处理 |
| FLA delta-rule 独立探针 | 所测输出与梯度对照通过 | 不能只因模型含线性注意力就归因于 FLA |
| 同一数据换用户态栈 | 完成训练、评估及保存 | 数据损坏不足以解释原有故障 |
| 同宿主驱动,旧栈回退后训练 | 100 步通过 | 无需先假设必须升级宿主驱动 |
| 新栈不增加 allocator 调参 | 100 步无 fault | 单靠显存碎片解释故障不充分 |
FLA 探针覆盖了长度 65、129、3700,检查输出与五组关键梯度。跨过分块长度附近的样例,比只测一个整齐的短序列更有信息;但探针通过只证明这些输入与调用组合没有暴露问题,并不能排除完整训练中的其他路径。
GPU 执行具有异步性,报错暴露的 Python 行未必就是实际发生非法访存的位置。排查时先缩小到前向、反向或优化器阶段,再检查具体 shape、dtype、转置和库选择,不宜直接修改报错行附近的模型公式。
三、为什么独立 GEMM 通过,训练仍然会 fault
Qwen3.5 在这套配置中混合使用线性注意力和常规 attention;LoRA、反向传播、动态长度与梯度累积又带来不同的矩阵形状和内存布局。另一个模型可以训练,只能证明设备能执行那组负载,不能覆盖当前模型的全部 kernel 组合。
ROCm 上游 PR #8909 描述了与本次现象高度一致的机制:gfx1201 的 GLTr 转置读取在部分 tile 上可能越过张量末尾。若后面仍是已映射空间,独立测试可能正常;若张量紧靠未映射页,越界读取才表现为 memory fault。上游使用 guard-page 测试,使这种读取不能被分配器留下的空隙掩盖。
这也解释了为何改变 batch、分配策略或临时张量后,失败步数可能变化。它们改变了触发条件,不等于修复了底层读取。empty_cache() 或 allocator 配置可以用于对照,但不能仅凭故障暂时消失就视为修复。
本地没有逐 commit 重建并二分 hipBLASLt,因此结论保留为“旧用户态 hipBLASLt 路径与该类越界问题高度一致”,不将整个修复归功于单一提交。
四、回退必须确认执行路径真的改变
最初只设置 PyTorch 的 BLAS 偏好,训练仍然失败。库日志显示,rocBLAS 仍可能把运算委派给 hipBLASLt。因此旧栈的有效回退需要同时设置:
export TORCH_BLAS_PREFER_HIPBLASLT=0
export ROCBLAS_USE_HIPBLASLT=0
export DISABLE_ADDMM_HIP_LT=1三项配置分别影响所测版本中的不同入口,不能把它们当成一个重复开关。rocBLAS 官方文档明确提供了后端选择控制;另外两项也需要结合当时 PyTorch 的实现验证,不能保证所有版本都按相同方式处理。
对这次实验,关键结果是库日志中不再出现 hipBLASLt solution,且完整 100 步训练通过。只有环境变量存在、没有实际调用证据,仍不足以说明回退生效。后续新栈测试则反过来确认 hipBLASLt solution 被调用,再验证它可以完成训练。
这形成了三组互相支持的结果:旧栈正常路径失败,旧栈绕开该路径后稳定,新栈恢复该路径后仍稳定。它比连续修改 batch、显存参数和模型配置后偶然跑通,更容易解释。
五、稳定以后,再恢复与优化计算路径
训练运行时中的性能改进并非一次升级带来的单一收益,而是逐层处理不同瓶颈:
| 改动 | 解决的问题 | 需要保留的检查 |
|---|---|---|
| 视觉 patch embedding Conv3D→Linear | 利用独立 patch 的结构,绕开慢卷积实现 | 前向、输入梯度、权重梯度与参数身份 |
| 接入 FLA 0.5.2 | 加速 Qwen3.5 的 gated delta-rule 计算 | 模型模块实际绑定到 FLA,而非只安装包 |
| 旧栈关闭 hipBLASLt | 暂时绕开故障路径 | 库日志与完整训练是否通过 |
| 固定新用户态栈并恢复 hipBLASLt | 在稳定前提下使用高效 GEMM 路径 | 新路径实际调用、loss、评估与保存 |
视觉层的数学条件与跨平台复用见 视觉 patch embedding 的等价改写:从 Conv3D 到 Linear。该补丁也用于 CUDA 训练镜像,不能把所有相关收益都记为 AMD 后端优化。
FLA 则只作用于模型相应的线性注意力计算。当前记录中 chunk_gated_delta_rule 已实际绑定;未安装 causal_conv1d,短卷积部分仍走 torch。因此日志提示完整 fast path 不可用,不等于 FLA 完全没有参与计算。反过来,import fla 成功,也不能证明模型已采用它。
为了保持调用关系可核对,镜像同时固定 Transformers、Triton 与 FLA 版本。仅升级某一个包可能改变可选依赖检测或模型绑定,需要检查实际模块,再运行前反向对照。
六、性能与数值结果放在同一张表里
以下 100 步对照固定多图输入、预处理缓存、cutoff 4096、LoRA、batch=1、梯度累积 8、BF16、梯度检查点和 seed。平均耗时按记录的训练总 runtime 除以步数计算,不等同于排除评估、保存与预热后的纯 kernel 时间。
| 路线 | 完成情况 | 训练 runtime | 平均耗时 | train loss |
|---|---|---|---|---|
| 旧栈正常路径 | 第 2~8 步 fault | 未完成 | 不作完整训练比较 | 未完成 |
| 旧栈三变量回退,FLA 0.5.2 | 100/100 | 2377 s | 23.77 s/步 | 1.0896 |
| 固定新栈,hipBLASLt 开启,FLA 0.5.2 | 100/100 | 619.9 s | 6.20 s/步 | 1.0899 |
两条稳定路线的总时长比约为 3.83,新栈耗时减少约 74%。这是同配置下的整套用户态路径对比,没有单独测出每个库版本贡献了多少收益。
评估 loss 在第 20/40/60/80/100 步分别约为 1.655/1.554/1.572/1.613/1.6727 和 1.656/1.551/1.568/1.609/1.6633。两条轨迹接近,checkpoint 与最终保存也完成;小样本重复训练后评估 loss 回升,则应继续按过拟合检查,不能仅凭 loss 下降证明模型任务能力提升。
新栈随后对 Qwen3-VL 做了真实多模态冒烟,训练与评估 loss 接近旧记录。其速度对照存在共享负载下的明显波动,因此没有把新旧差值确定为性能回归。
其他记录中的“约 190 s/步降到 2.94 s/步”属于 CUDA 训练镜像同时更新 PyTorch、FLA 和视觉补丁的结果,不属于这组 R9700 对照。视觉改写的 20 步总时长测试,也采用另一组模型和测试口径;这些倍率不能相乘或串成同一次训练的提速曲线。
七、把经验固化到训练运行时
归档中完成 100 步验证的关键组合为:
Python 3.11
torch 2.15.0a0+rocm10.2.0a20260922
torchvision 0.30.0a0+rocm10.2.0a20260922
Triton 3.8.0
transformers 5.6.0
FLA 0.5.2这是当时验证过的组合,不是要求后续所有 R9700 使用同一个 nightly。镜像应固定日期、wheel 来源和 digest,避免浮动标签悄悄改变环境。宿主驱动版本与容器用户态版本分别记录;本次保持宿主不变即可修复,也不意味着任意新用户态都能兼容旧驱动。
镜像中的 MIOpen 禁用守卫、视觉等价补丁、空媒体检查和依赖约束各有用途。纯文本使用多模态模板时,要区分没有图片与传入空视觉张量;只清理上层字段,却继续调用视觉模型,不是有效的文本对照。补丁是否应用也要在构建阶段检查,不能只看 Dockerfile 中出现了命令。
新环境进入默认训练路径前,依次验证设备可用、BF16 Linear 前反向及实际 BLAS 路径、FLA 输出与梯度、真实多模态训练,以及评估和 checkpoint。需要同时记录 GPU fault、allocator failure 和 NaN/Inf,成功结束只是其中一项。
本次没有通过反复清空缓存或增加显存参数掩盖问题,而是先建立可完成的回退基线,再恢复高效路径。后续性能优化也应保留这个顺序:先确认计算正确和长于原故障窗口的稳定性,再逐项比较吞吐、显存与任务效果。