MegaThinking

better tokens, better intelligence, contributing superior tokens to models

1. PPO

Proximal Policy Optimization Algorithms

两阶段循环

为什么可以“多轮”

通常情况下,如果对同一批数据进行多轮优化,策略会因为更新过头而崩溃。但 PPO 引入了 Clipped Objective(裁剪目标函数)

  • 安全护栏:在每一轮优化中,PPO 会计算新策略和采样时的旧策略的概率比。如果这个比值超出了设定的范围(比如 0.81.20.8 \sim 1.2),梯度就会被“截断”。
  • 效果:这确保了即使在这一批数据上反复“薅羊毛”优化,新策略也不会跑得离旧策略太远,从而保证了训练的稳定性。

1.1. 采样阶段 (Sampling Phase)

  • 动作:让当前的策略 πθold\pi_{\theta_{old}} 在环境中运行一段时间。
  • 产出:收集一批轨迹数据(包括状态 ss、动作 aa、奖励 rr 等)。
  • 性质:这些数据是“新鲜”的,反映了当前策略的行为模式。

在这个阶段,神经网络的参数是固定不动的(即 θold\theta_{old})。Actor (策略网络):在环境中根据概率分布选择动作。数据收集:把 (st,at,rt,st+1)(s_t, a_t, r_t, s_{t+1}) 存入一个临时的 Buffer。目标:收集足够数量的轨迹(比如 2048 个时间步)。

1.1.1. 计算“标签” (Preprocessing)

在开始训练前,利用收集到的数据计算两个关键值:

  • A^t\hat{A}_t (Advantage):优势函数,用来衡量这个动作比平均水平好多少。
  • RtR_t (Returns):这一步动作带来的累积奖励。

注意到

  • rt(θ)r_t(\theta):新旧策略概率比(用于 Actor)。
  • A^t\hat{A}_t:优势估计(用于 Actor,决定更新方向)。
  • RtR_t:回报目标值(用于 Critic,提升估值精度)。

如果只用即时奖励 rtr_t 作为目标,Critic 就会变得非常“短视”。即时奖励 rtr_t:只代表当前这一步的好坏。回报目标 RtR_t:代表从当前时刻起,在轨迹剩余部分上累计(折现)后的总回报。目标是让 Critic 具备“向前看”的能力,因此用 V(st)V(s_t) 去拟合这个 RtR_t

Rt=A^t+V(st)R_t = \hat{A}_t + V(s_t)

RtR_t (Returns):作为 Critic 网络的监督信号(标签)。

  • 计算逻辑:通过 A^t\hat{A}_t(优势)与采样时旧的 V(st)V(s_t) 相加得到:Rt=A^t+V(st)R_t = \hat{A}_t + V(s_t)
  • 物理意义:它代表了在当前策略下,从状态 sts_t 开始预期能获得的折现总奖励。Critic 的优化目标就是让预测值 Vθ(st)V_\theta(s_t) 尽可能接近这个 RtR_t

这意味着:

  1. 先用 GAE 算出了优势估计 A^t\hat{A}_t
  2. 通过 A^t+V(st)\hat{A}_t + V(s_t),便可反向推导出这一步动作对应的“目标回报” RtR_t
  3. 价值损失 (Value Loss) 就变成了:MSE(Vnew(st),Rt)MSE(V_{new}(s_t), R_t)

1.1.2. 小结

  • Actor:利用 A^t\hat{A}_t(相对好坏)来决定 θ\theta 的更新方向。
  • Critic:利用 RtR_t(绝对得分)来修正自己对世界的认知。

1.2. 优化阶段 (Optimization Phase)

多轮优化 (Several Epochs)

  • 动作:将刚才采样的这一批数据反复输入神经网络进行多次梯度更新。
  • 关键点:在传统的 On-policy 算法(如普通的策略梯度)中,这批数据更新一次就必须扔掉。但 PPO 允许在同一批数据上跑 3 轮、5 轮甚至 10 轮(Epochs)。

要把 Buffer 里的数据,分成更小的 Mini-batches,重复训练 KK 个 Epochs(比如 K=10K=10)。在每一个 Epoch 里的微观操作:计算概率比 rt(θ)r_t(\theta):用当前正在更新的 θ\theta 计算动作概率,除以采样时的 θold\theta_{old} 计算的概率。应用裁剪 CLIPCLIP:如果 rt(θ)r_t(\theta) 偏离 1 太远(比如超过 20%),就强行截断。梯度更新:通过反向传播更新参数 θ\theta

为什么 rt(θ)r_t(\theta) 允许“多轮更新”。PPO 能够从 On-policy 转向近乎 Off-policy 的理论支柱,PPO 本质上是利用了重要性采样技术。

  • 理论背景:优化目标是新策略 πθ\pi_\theta,但训练数据来自旧策略 πθold\pi_{\theta_{old}} 的采样分布。
  • 补偿机制:通过概率比率 rt(θ)r_t(\theta),对数据分布偏差做重要性采样修正。
  • 约束:重要性采样要求两个分布不能差太远,否则方差会爆炸。这正是 LCLIPL^{CLIP} 存在的根本原因——它在数学上维护了重要性采样的有效区间。

1.2.1. 优势估计

通常采用 GAE (Generalized Advantage Estimation)。

简单来说,优势函数 A^t\hat{A}_t 的目标是回答:“在状态 sts_t 下采取动作 ata_t,比平均情况(即 Baseline)好多少?”

1.2.1.1. 计算时序差分残差(Temporal Difference Error)

首先计算每一个时间步的即时偏差 δt\delta_t。它衡量了“实际观测到的奖励 + 下一步的估值”与“当前估值”之间的差距:

δt=rt+γV(st+1)V(st)\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t)

  • rtr_t:当前步获得的奖励。
  • V(st+1)V(s_{t+1}):神经网络(Critic)对下一步状态的估值。
  • V(st)V(s_t):神经网络(Critic)对当前状态的估值。

1.2.1.2. 累加衰减

[0, T)

优势估计 A^t\hat{A}_t 不是只看当前这一步,而是要把未来的 δ\delta 都考虑进来,但要进行指数衰减。公式如下:

A^t=δt+(γλ)δt+1+(γλ)2δt+2++(γλ)T1tδT1\hat{A}_t = \delta_t + (\gamma\lambda)\delta_{t+1} + (\gamma\lambda)^2\delta_{t+2} + \cdots + (\gamma\lambda)^{T-1-t}\delta_{T-1}

这里有两个关键的超参数:

  • γ\gamma (Gamma):折扣因子(通常 0.99),决定了对远期奖励的重视程度。
  • λ\lambda (Lambda):GAE 因子(通常 0.95),用于在偏差(Bias)和方差(Variance)之间做权衡。

实现时,逆序(t)计算

  • 如果 λ=0\lambda = 0A^t=δt\hat{A}_t = \delta_t。这叫 1-step TD。它很稳定(方差小),但如果 VV 函数估值不准,它就会错得离谱(偏差大)。

  • 如果 λ=1\lambda = 1A^t\hat{A}_t 变成了从当前步到截断点 TT 的所有奖励累加。这很真实(无偏差),但环境随机性太强,导致数值跳变剧烈(方差大)。

这就是 λ\lambda 用于在偏差(Bias)和方差(Variance)之间做权衡的物理意义。PPO 选取 λ=0.95\lambda = 0.95 它在“相信神经网络的估值”和“相信实际观测到的奖励”之间取了一个折中。

1.2.1.3. 标准化 (Advantage Normalization)

在算出 TT 个时间步的所有 A^t\hat{A}_t 后,工程上通常会进行一次标准化处理:

A^t=A^tmean(A^)std(A^)+108\hat{A}_t = \frac{\hat{A}_t - \text{mean}(\hat{A})}{\text{std}(\hat{A}) + 10^{-8}}

  • 稳定梯度:在一个 Batch 中,优势值的数值跨度可能很大。标准化后,它们的均值为 0,标准差为 1。
  • 逻辑闭环:这确保了在一个 Batch 里,大约有一半的动作会被认为是“好于平均”(正值,增加概率),另一半是“差于平均”(负值,减小概率)。这对于 Adam 优化器的稳定收敛极其重要。

1.2.1.4. 总结计算流程

rt(θ)=πθ(atst)πθold(atst)r_t(\theta) = \frac{\pi_\theta(a_t | s_t)}{\pi_{\theta_{old}}(a_t | s_t)}

  1. 运行 TT 步采样,收集所有的 rr 概率比例和 VV 状态价值。
  2. 从后往前计算(这样可以用 At+1A_{t+1} 算出 AtA_t):
  • At=δt+(γλ)At+1A_t = \delta_t + (\gamma\lambda) A_{t+1}
  1. 对整个 Batch 进行标准化。
  2. 将算好的 A^\hat{A} 输入 LCLIPL^{CLIP} 进行优化。

1.2.2. 损失函数

优势估计 A^t\hat{A}_t 和概率比率 rt(θ)r_t(\theta) 都准备好了,进入 PPO 执行阶段构建 Loss 函数并进行参数更新

Adam 优化器并不是只优化策略,它其实是在同时优化三个目标。

总损失函数 LtCLIP+VF+SL^{CLIP+VF+S}_t 通常长这样:

Lttotal(θ)=LtCLIP(θ)c1LtVF(θ)+c2S[πθ](st)L_t^{total}(\theta) = L_t^{CLIP}(\theta) - c_1 L_t^{VF}(\theta) + c_2 S[\pi_\theta](s_t)

LCLIP(θ)=E^t[min(rt(θ)A^t,clip(rt(θ),1ϵ,1+ϵ)A^t)]L^{CLIP}(\theta) = \hat{\mathbb{E}}_t \left[ \min \left( r_t(\theta) \hat{A}_t, \text{clip}(r_t(\theta), 1 - \epsilon, 1 + \epsilon) \hat{A}_t \right) \right]

这三个部分分工明确:

  • LtCLIP(θ)L_t^{CLIP}(\theta) (策略损失):利用 A^t\hat{A}_trt(θ)r_t(\theta) 进行裁剪优化。直观上,它会在优势为正时提高动作概率,同时用 clip 限制单次更新幅度,避免策略变化过快。
  • LtVF(θ)L_t^{VF}(\theta) (价值损失):通常是均方误差 MSE(Vθ(st),Vtarget)MSE(V_\theta(s_t), V_{target})。它负责让 Critic 的状态价值预测更贴近目标回报(更准确)。
  • S[πθ](st)S[\pi_\theta](s_t) (熵奖励):鼓励策略保持一定的随机性,避免分布过早塌缩到少数动作上,从而保留探索空间。

MSE 均方误差

1.2.2.1. 执行 Adam 更新

Adam 优化器, 梯度下降 (Gradient Descent)

有了总损失后,流程如下:

  1. 计算梯度:对总损失关于参数 θ\theta 求导(即之前提到的 L wrt θL \text{ wrt } \theta)。
  2. 反向传播:将梯度传回神经网络。
  3. 参数更新:Adam 优化器根据动量和自适应学习率微调 θ\theta

进入 KK 个 Epoch 的循环

针对同一批采样数据(那 NTNT 个样本),反复进行 KK 次上述的“计算 Loss -> 更新参数”过程。

  • 在第 1 遍时:rt(θold)=1r_t(\theta_{old}) = 1,概率比退化为恒等映射,更新等价于未做分布修正的常规梯度步。
  • 在第 KK 遍时:由于参数已经改了好几次,新旧策略的偏差 rt(θ)r_t(\theta) 可能会很大。这时候 Clipping(裁剪) 就会大显身手,强行把那些偏移过大的梯度归零,防止模型跑飞。

注:虽然 PPO 的理论目标是最大化奖励,但在代码实现中,通常会对总目标函数取负值,将其转化为最小化损失,从而用 Adam 等优化器做梯度更新。

1.2.2.2. 更新旧策略

θoldθ\theta_{old} \leftarrow \theta

KK 次迭代结束,这一批数据的价值就被“榨干”了。
此时,把当前的最新参数 θ\theta 赋值给 θold\theta_{old}。然后清空缓存的数据,回到环境里,开启下一轮 N×TN \times T 的数据采集。

1.3. Hyperparameters 参考

参数 常用值 作用
ϵ\epsilon 0.10.20.1 \sim 0.2 裁剪阈值,限制单次更新步长
γ\gamma 0.990.99 长期奖励折扣因子
λ\lambda 0.950.95 GAE 平衡因子
c1c_1 0.50.5 价值损失权重(MSE 权重)
c2c_2 0.010.01 熵系数(鼓励探索,防止过早收敛)
KK 3103 \sim 10 每个 Batch 的重复训练次数(Epochs)

2. RLHF 中的 PPO

在很多 LLM 对齐/偏好优化的工程实现里,会看到 “PPO + reference model(参考模型)”。这很容易让人误以为 reference model 是 PPO 论文(Schulman 2017)的一部分;但严格来说,它是 RLHF 场景下额外加入的约束/正则,用来防止策略为了刷 reward 而跑飞(reward hacking、语言退化、分布崩坏等)。

2.1. RLHF 训练 flow

SFT → RM → PPO

可以把最常见的 RLHF 流程理解成三段:

  1. SFT:用高质量指令数据把模型先教会“基本说话方式”,得到 πSFT\pi_{\text{SFT}};它常常也会作为后面的 πref\pi_{ref}(冻结参考模型)
  2. Reward Model(RM):用偏好数据训练一个打分器 R(x,y)R(x,y)(或 rϕ(x,y)r_\phi(x,y)),用于刻画“在相同输入下,哪些输出更受偏好”。
  3. PPO-RLHF:从 πSFT\pi_{\text{SFT}} 初始化可训练策略 πθ\pi_\theta,用 PPO 提高 RR,同时用 KL-to-referenceπθ\pi_\theta 拴在 πref\pi_{ref} 附近。

PPO-RLHF 的实现,通常就是把“文本生成”当成一条轨迹上的序列决策,然后复用前边提到的 PPO 两阶段循环:

  • 自回归 MDP(最常见的设定):第 tt 步的“动作”是下一个 token yty_t;状态可以抽象成 (x,y<t)(x,y_{<t})
  • Rollout:用 πθold\pi_{\theta_{old}} 采样一批 completions(得到 token 轨迹与 logprob)。
  • Reward / shaping:把 RM 分数与 KL shaping 组合成每步可用的标量回报信号(工程上常见是把 KL 摊到 token;RM 可能是序列末一次性给分,也可能有更细的 shaping,取决于实现)。
    • reward shaping 在这里可以直观理解为:不只给“最后好不好”的稀疏信号,而是额外构造/改写一组更密、更及时的逐步回报,让 PPO 在生成过程中更容易学、也更可控;其中 per-token 的 KL 项就是很典型的 shaping。
    • RM shaping 则更具体:指把 reward model 的偏好信号从“只在结尾给一次分”,扩展成更稠密的过程性反馈(例如分段打分、对关键子结构/步骤给增量奖励、或把可验证规则与 RM 组合成逐步项)。不同系统差异很大;设计不当也可能让模型去“刷 RM shaping”而不是真正提升偏好质量,因此通常仍会配合 KL-to-reference 与谨慎的系数/裁剪。
  • Optimization:在同一批数据上算优势(GAE)、跑 LCLIPL^{CLIP} + value loss + entropy,重复 KK 个 epoch;最后更新 θoldθ\theta_{old}\leftarrow\theta,进入下一轮 rollout。

一句话总结:RM 给方向,πref\pi_{ref} + KL 给长期护栏,PPO(尤其 clipping)给短期稳定更新

2.2. 两个“旧策略”不要混

PPO 里几乎总会涉及旧策略,但它通常指的是:

  • πθold\pi_{\theta_{old}}(PPO 的 old policy):上一轮采样用的策略快照,用于重要性采样比率

    rt(θ)=πθ(atst)πθold(atst)r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}

    它是 每一轮都会更新 的。

而 RLHF 工程里常说的 reference model 一般是:

  • πref\pi_{ref}(RLHF 的 reference policy/model):冻结的锚点模型(常见做法是 SFT 后的模型),用于给当前策略加一个 “别偏太远” 的约束;它通常在一段训练期间 保持不变 或更新频率很低。

2.3. KL-to-reference:把“别跑飞”写进目标

以 PPO-RLHF 常见写法为例,会把 reward 加上一个 KL 惩罚(或等价的 reward shaping):

R(x,y)=R(x,y)βKL(πθ(x)  πref(x))R'(x, y) = R(x, y) - \beta \, \mathrm{KL}\left(\pi_\theta(\cdot|x)\ \|\ \pi_{ref}(\cdot|x)\right)

这里的符号可以按“一条 RLHF 训练样本”来理解:

  • xx:prompt / 输入上下文(用户问题、题目、对话历史等)
  • yy:response / 输出序列(模型在 xx 条件下生成的整段回答 token 序列)
  • R(x,y)R(x, y):在输入 xx 下输出 yy 的奖励(来自 reward model、规则打分等)
  • KL(πθ(x)  πref(x))\mathrm{KL}(\pi_\theta(\cdot|x)\ \|\ \pi_{ref}(\cdot|x)):在同一个输入 xx 条件下,当前策略相对 reference policy 的分布偏离程度

于是 PPO 实际最大化的是 “奖励 - 偏离 reference 的代价”。直觉上:

  • 如果只追 RR:模型会倾向于钻 reward 的空子,偏离语言先验越来越大。
  • 加上 KL:reference model 提供了一个长期锚点,PPO 的 clipped update 提供了一个短期的“每步别迈太大”,两者一起让训练更稳。

备注:不同实现里 KL 可能以多种形式进入(显式 KL penalty、或把 per-token logprob 差写进 reward),但核心都是 “把策略拴在 πref\pi_{ref} 附近”。

一个常见的工程视角是把 KL “摊平”到 token 级别。设输出序列 y=(y1,,yT)y=(y_1,\dots,y_T),则

在自回归语言模型里,这里的 时间步 tt 通常就是“生成第 tt 个 token 的那一步”(也就是 token index):

  • yty_t:第 tt 步采样得到的那个 token
  • y<t=(y1,,yt1)y_{<t}=(y_1,\dots,y_{t-1}):到第 tt 步之前已经生成的前缀(第 1 步时为空前缀)

因此 TT 就是这条输出序列的长度(token 数)。这和传统 RL 里“环境每走一步”的时间轴可以不同:在 LLM 文本生成里,“一步”往往等价于“再生成一个 token”

logπθ(yx)logπref(yx)=t=1T(logπθ(ytx,y<t)logπref(ytx,y<t))\log \pi_\theta(y|x) - \log \pi_{ref}(y|x) = \sum_{t=1}^T \Big(\log \pi_\theta(y_t|x,y_{<t}) - \log \pi_{ref}(y_t|x,y_{<t})\Big)

如果只关心当前采样到的这条序列(on-policy 轨迹)上的惩罚,那么很多实现会定义一个 token 级别的“KL 代价”:

rtKLβ(logπθ(ytx,y<t)logπref(ytx,y<t))r^{KL}_t \triangleq -\beta\Big(\log \pi_\theta(y_t|x,y_{<t}) - \log \pi_{ref}(y_t|x,y_{<t})\Big)

这里的 logπθ(ytx,y<t)\log \pi_\theta(y_t|x,y_{<t})(logprob)就是:策略模型在时间步 tt 给出的“下一个 token”的条件概率分布 πθ(x,y<t)\pi_\theta(\cdot|x,y_{<t}) 中,取到实际 token yty_t 的概率再取对数(通常取自然对数)。

然后把它加进每一步的 reward(reward shaping)。这样累加起来就是序列级别的 logprob 差惩罚:

t=1TrtKL=β(logπθ(yx)logπref(yx))\sum_{t=1}^T r^{KL}_t = -\beta\Big(\log \pi_\theta(y|x) - \log \pi_{ref}(y|x)\Big)

直觉上:如果某个 token 在当前策略下的概率比 reference 更大(logπθlogπref>0\log \pi_\theta - \log \pi_{ref} > 0),那它会产生负的 shaping reward(惩罚),从而抑制策略在该方向上“越走越远”。

2.4. 推荐阅读

  • Ouyang et al., 2022. Training language models to follow instructions with human feedback (InstructGPT).(SFT → RM → PPO,以及 KL/reference 的由来)
  • Stiennon et al., 2020. Learning to summarize with human feedback.(更早期、端到端的 RLHF 案例)
  • Ziegler et al., 2019. Fine-Tuning Language Models from Human Preferences.(偏好优化 + KL 正则的直观版本)

扩展(对比视角,理解“reference 并非 PPO 专属”):

  • Rafailov et al., 2023. Direct Preference Optimization (DPO).(绕开 RM 与 PPO,但同样体现 anchor/reference 的思想)

3. GRPO:从 PPO / RLHF 再往前走一小步

前文将 PPO 概括为“稳定的策略更新框架”,将 RLHF 概括为“RM + KL-to-reference + PPO”的常见落地形态。进一步地,在 数学推理 / 可验证奖励 这类场景里,训练目标仍然可以用 PPO 的 clipped objective,但 优势(advantage)与 baseline 的估计往往会变得更棘手。

GRPO(Group Relative Policy Optimization) 是在 DeepSeekMath 里提出的、PPO 的一个变体:动机之一是让 RL 在 LLM 场景里更省资源,同时处理 “reward 往往只在序列末出现、但 value 需要 token 级别监督” 这类不匹配。

后续再展开学习

  • 仍然很 PPO:整体还是围绕 clipped ratio 的策略更新思路在转(可以把它理解成“骨架仍在 PPO”)。
  • 关键变化:去掉 value模型 / critic:GRPO 不再额外训练一个与 policy 同量级的 value function 来给每个 token 做 baseline。
  • 用 group 做相对基线:对同一个问题 qq,先从旧策略采样一组输出 {o1,,oG}\{o_1,\dots,o_G\},再用 组内相对比较 来构造优势(论文强调这与 reward model 常见的“同题对比训练”更一致)。
  • KL 处理方式也可能不同:论文里也讨论了与 PPO 场景下 KL penalty 不同的正则化思路(读 4.1 小节时对照实现会更清晰)。

在 KubeRay 里,Ray Job 由 Ray Cluster 承载与管理,真正的难点往往在于 如何把 Ray Job 与 Ray Cluster 的生命周期对齐

rayjob-raycluster

先看通用 Job:常用 init container 拉数据、下发配置等;init 失败即整次 Job 失败,这很直观。

Ray Job 不同:它并不是「那一组实际跑在集群里的 Pod」——资源实体是 Ray Cluster;所谓 Ray Job 的 init container,也落在 Cluster 侧。结果是 Job 侧的 init 语义,会和 Cluster 的 init / bootstrap 语义绑在一起

  • 站在 Cluster 视角:init 失败时反复重试直到 bootstrap 成功,常常说得通——先得把集群建立起来。
  • 站在 Job 视角:Job 是一次性任务,init 失败更合理的预期是 fail fast,而不是长期跟着 Cluster 重试。

也是在借助大模型拆解 issue、梳理场景,并对照代码与线上行为逐项验证的过程中,才逐渐理解:Ray Job 对 init container 的生命周期管理,很难用一套简单直白、一步到位的规则实现。

1h vibe issue, 8h vibe coding
70M tokens cost

https://github.com/ray-project/kuberay/issues/4637

两种典型模式:

  • 新建 Ray Cluster:Ray Job 的 init container 会生效;在这条路径下,它实际就是跑在 Ray Cluster 上的 init container,与 Cluster 的 bootstrap 同一条链路。
  • 使用已有 Ray Cluster:Ray Job 的 init container 不生效;Job 只消费已有 Cluster,不会为本次 Job 再单独跑一轮 init。

Cluster 自身的生命周期也要分开看:

  • Job 新建的 Cluster:可用 Ray Job 的 delete rule 决定在 Job 结束后是否删除 Cluster / Workers 等;默认为保留 Cluster
  • 沿用已有 Cluster:Ray Job 结束 不改变 Cluster 的生命周期(Cluster 可能继续服务其他任务或由别处托管)。

回到 Ray Job 自定义 init 失败——这发生在 Job 新建并绑定的专属 Cluster 上。Job 结束后如果 Cluster 长期保留,语义上确实容易别扭;而更合理的模式通常是 短时间保留现场(以收集日志和进行问题诊断),然后自动回收这个临时 Cluster。这本质上是一种「单次 Job + 一套临时 Cluster」的短生命期部署方式,与长期共用 Cluster 是两套完全不同的心智模型。

不过需要注意,当前 Ray Job 实际上并不支持这样的能力:Job 自定义 init 失败后,Job 仍然处于 initializing 状态,系统还不会自动实现「短暂保留现场再回收 Cluster」的行为。可以继续 vibe issue, 基于 issue vibe coding。

尚未系统整理,先记一个粗判断:整体架构在走向成熟,部署形态普遍解耦——不止训推分离,还出现了 Agent 应用与训推平台、训推 API(如 Tinker API)、训推框架的分层

在这种形态下,常见会并行做两件事:一是用 OpenTelemetry(以 spans 为主) 做标准化 trace,沉淀模型 / Agent 的行为轨迹,再回流进 RL 训练闭环;二是通过 LLM Proxy 统一 Agent 侧使用的模型 API,在训练态把请求 转发到当前正在更新的模型,由它承担 RL 里的推理侧,避免应用侧调用与训练态模型服务两条路径对不齐。

关于轨迹记录与训练回流,直觉上和早年搜索推荐那一套并无本质不同:线上记录与埋点 → 数据回流 → 离线实验与训练

https://www.hiascend.com/document/detail/zh/mindcluster/70rc1/clustersched/dlug/mxdlug_007.html

有如下几类 configmap

  • cmDevice: ns, kube-system; cmName, mindx-dl-deviceinfo-{NodeName}; which is reported by device-plugin
  • cmNode: ns, mindx-dl; cmName, mindx-dl-nodeinfo-{NodeName}; which is reported by nodeD
  • cmPingMesh: ns, cluster-system; cmName, pingmesh-config;
  • cmSuperPodDevice: ns, cluster-system; cmName, super-pod-{SuperPodId}; clusterD 维护
    • 特别的 {RAS_NET_ROOT_PATH}/cluster/super-pod-{SuperPodId}/super-pod-{SuperPodId}.json; clusterD 维护
  • cmPubicFault: mc-consumer-publicfault=true label;

其中 cmDevice configmap mindx-dl-deviceinfo-{NodeName}, 由 device-plugin 上报, 包括如下信息

  • DeviceInfoCfg
  • SwitchInfoCfg

cmPubicFault configmap, 包括如下信息

  • PublicFault

pingmesh-config 的格式为 global pingmesh 任务的配置或者是指定 superpodid 的任务配置

1
2
3
4
{
"activate": "on",
"task_interval": 5
}

node annotation 中包括如下信息

  • product-serial-number
  • superPodID
  • baseDeviceInfos
  • serverType
  • serverIndex

THP

https://alexandrnikitin.github.io/blog/transparent-hugepages-measuring-the-performance-impact/

增加 page 大小, 从而减少 TLB 大小; 由于 walk TLB 开销较大, 所以是个优化

THP 会让 os 申请连续的内存空间大小, 但如果申请不到, 则 os 会开始 compact, reclaim or page out other pages;

That process is expensive and could cause latency spikes (up to seconds)

cat /proc/buddyinfo

Each column represents the number of pages of a certain order which are
available. In this case, there are 0 chunks of 2^0PAGE_SIZE available in
ZONE_DMA, 4 chunks of 2^1
PAGE_SIZE in ZONE_DMA, 101 chunks of 2^4*PAGE_SIZE
available in ZONE_NORMAL, etc…

https://andorian.blogspot.com/2014/03/making-sense-of-procbuddyinfo.html

https://wangcong.net/article/FPandBP.html

pathways

https://blog.research.google/2022/04/pathways-language-model-palm-scaling-to.html

a single model that could generalize across domains and tasks while being highly efficient. An important milestone toward realizing this vision was to develop the new Pathways system to orchestrate distributed computation for accelerators.

few-shot

TPU v4 Pods

Pipelining is typically used with DCN

word to vector, This vector represents the word’s meaning and context within the given language

embedding layer, lookup table

Positional encoding

https://medium.com/@tech-gumptions/transformer-architecture-simplified-3fb501d461c8

This means that the output of a layer is added to the initial input, allowing the model to learn to only make small changes to the input

The decoder’s job is to produce the English sentence based on both the original French sentence and the bits of the English sentence it has generated so far.

Input Embedding: Just as with the Encoder, the input to the Decoder (which is the target sequence during training) is first embedded into continuous vectors.

It’s important to note that this masking is only applied during training. During inference, the decoder can attend to all words in the target sequence, including future words.

To summarize, the Decoder in the Transformer architecture processes its input through self-attention, cross-attention with the Encoder’s output, and position-wise Feed-Forward networks, repeatedly for each stacked block, culminating in a final output sequence after the softmax operation.

https://jalammar.github.io/illustrated-transformer/

https://nlp.seas.harvard.edu/2018/04/03/attention.html

https://jalammar.github.io/illustrated-gpt2/

PPO/GRPO/重要性采样/拒绝采样

megatron 保存 ckpt 原理

多节点测试依赖 mpi。编译时打开 MPI 开头。

测试时配置多节点 ssh 免密。另外如果是 RoCE 网络,注意正确配置 NCCL 无损队列匹配 RoCE 无损队列。

逐渐调大 size 衡量网络带宽情况。

https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html#nccl-algo

NCCL_ALGO=ring 衡量网络带宽时较为稳定。

https://github.com/NVIDIA/go-nvml

The nvml.h file is a direct copy of nvml.h from the NVIDIA driver. Since the NVML API is guaranteed to be backwards compatible, we should strive to keep this always up to date with the latest.

https://github.com/xlab/c-for-go.git

golang cgo

https://www.rectcircle.cn/posts/go-static-compile-and-cgo

https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-05-internal.html

poc env, windows11 + wsl2 ubuntu 18.04

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
~/projects/go-nvml ❯ nvidia-smi
Sun Dec 17 18:57:57 2023
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 545.29.04 Driver Version: 546.17 CUDA Version: 12.3 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 NVIDIA GeForce RTX 3070 Ti On | 00000000:06:00.0 On | N/A |
| 0% 33C P8 13W / 290W | 1139MiB / 8192MiB | 1% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| 0 N/A N/A 23 G /Xwayland N/A |
+---------------------------------------------------------------------------------------+

test code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"log"
"os"

"github.com/NVIDIA/go-nvml/pkg/nvml"
)

func getNvidiaDeviceCount() {
ret := nvml.Init()
if ret != nvml.SUCCESS {
log.Fatalf("Unable to initialize NVML: %v", nvml.ErrorString(ret))
}
count, ret := nvml.DeviceGetCount()
if ret != nvml.SUCCESS {
log.Fatalf("Unable to get device count: %v", nvml.ErrorString(ret))
}
fmt.Printf("%d\n", count)
}

func main() {
args := os.Args
if len(args) < 2 {
fmt.Println("hello")
} else {
getNvidiaDeviceCount()
}
}

build commands

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export CGO_LDFLAGS="-Wl,-z,now"

go build main.go
./main
./main: symbol lookup error: ./main: undefined symbol: nvmlGpuInstanceGetComputeInstanceProfileInfoV

./main fake
./main: symbol lookup error: ./main: undefined symbol: nvmlGpuInstanceGetComputeInstanceProfileInfoV

# now to lazy
export CGO_LDFLAGS="-Wl,-z,lazy"
go build main.go
./main
hello

./main fake
1

go clean --cache && rm -rf main
go build -work -x main.go

go build -x

1
2
3
4
5
6
7
8
9
10
11
12
cd /root/go/pkg/mod/github.com/!n!v!i!d!i!a/go-nvml@v0.12.0-1/pkg/nvml
TERM='dumb' CGO_LDFLAGS='"-Wl,-z,lazy" "-Wl,--unresolved-symbols=ignore-in-object-files" "-Wl,--unresolved-symbols=ignore-in-object-files"' /root/tools/go/pkg/tool/linux_amd64/cgo -objdir $WORK/b002/ -importpath github.com/NVIDIA/go-nvml/pkg/nvml -- -I $WORK/b002/ -g -O2 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 ./cgo_helpers.go ./const.go ./init.go ./nvml.go
cd $WORK/b002
TERM='dumb' gcc -I /root/go/pkg/mod/github.com/!n!v!i!d!i!a/go-nvml@v0.12.0-1/pkg/nvml -fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=$WORK/b002=/tmp/go-build -gno-record-gcc-switches -I ./ -g -O2 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -o ./_x001.o -c _cgo_export.c
TERM='dumb' gcc -I /root/go/pkg/mod/github.com/!n!v!i!d!i!a/go-nvml@v0.12.0-1/pkg/nvml -fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=$WORK/b002=/tmp/go-build -gno-record-gcc-switches -I ./ -g -O2 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -o ./_x002.o -c cgo_helpers.cgo2.c
TERM='dumb' gcc -I /root/go/pkg/mod/github.com/!n!v!i!d!i!a/go-nvml@v0.12.0-1/pkg/nvml -fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=$WORK/b002=/tmp/go-build -gno-record-gcc-switches -I ./ -g -O2 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -o ./_x003.o -c const.cgo2.c
TERM='dumb' gcc -I /root/go/pkg/mod/github.com/!n!v!i!d!i!a/go-nvml@v0.12.0-1/pkg/nvml -fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=$WORK/b002=/tmp/go-build -gno-record-gcc-switches -I ./ -g -O2 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -o ./_x004.o -c init.cgo2.c
TERM='dumb' gcc -I /root/go/pkg/mod/github.com/!n!v!i!d!i!a/go-nvml@v0.12.0-1/pkg/nvml -fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=$WORK/b002=/tmp/go-build -gno-record-gcc-switches -I ./ -g -O2 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -o ./_x005.o -c nvml.cgo2.c
TERM='dumb' gcc -I /root/go/pkg/mod/github.com/!n!v!i!d!i!a/go-nvml@v0.12.0-1/pkg/nvml -fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=$WORK/b002=/tmp/go-build -gno-record-gcc-switches -I ./ -g -O2 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -DNVML_NO_UNVERSIONED_FUNC_DEFS=1 -o ./_cgo_main.o -c _cgo_main.c
cd /root/projects/go-nvml
TERM='dumb' gcc -I /root/go/pkg/mod/github.com/!n!v!i!d!i!a/go-nvml@v0.12.0-1/pkg/nvml -fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=$WORK/b002=/tmp/go-build -gno-record-gcc-switches -o $WORK/b002/_cgo_.o $WORK/b002/_cgo_main.o $WORK/b002/_x001.o $WORK/b002/_x002.o $WORK/b002/_x003.o $WORK/b002/_x004.o $WORK/b002/_x005.o -Wl,-z,lazy -Wl,--unresolved-symbols=ignore-in-object-files -Wl,--unresolved-symbols=ignore-in-object-files
TERM='dumb' /root/tools/go/pkg/tool/linux_amd64/cgo -dynpackage nvml -dynimport $WORK/b002/_cgo_.o -dynout $WORK/b002/_cgo_import.go
  1. /tmp/go-build2475505462/b002/nvml.cgo1.go
  2. /tmp/go-build2475505462/b002/nvml.cgo2.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CGO_NO_SANITIZE_THREAD
void
_cgo_c813f6172e91_Cfunc_nvmlGpuInstanceGetComputeInstanceProfileInfoV(void *v)
{
struct {
nvmlGpuInstance_t p0;
unsigned int p1;
unsigned int p2;
nvmlComputeInstanceProfileInfo_v2_t* p3;
nvmlReturn_t r;
char __pad28[4];
} __attribute__((__packed__, __gcc_struct__)) *_cgo_a = v;
char *_cgo_stktop = _cgo_topofstack();
__typeof__(_cgo_a->r) _cgo_r;
_cgo_tsan_acquire();
_cgo_r = nvmlGpuInstanceGetComputeInstanceProfileInfoV(_cgo_a->p0, _cgo_a->p1, _cgo_a->p2, _cgo_a->p3);
_cgo_tsan_release();
_cgo_a = (void*)((char*)_cgo_a + (_cgo_topofstack() - _cgo_stktop));
_cgo_a->r = _cgo_r;
_cgo_msan_write(&_cgo_a->r, sizeof(_cgo_a->r));
}

ChatGPT 3.5

-Wl,-z,lazy, -Wl,-z,now

-Wl,-z,lazy: The -Wl,-z,lazy flag in the gcc command is a linker option used to instruct the linker to utilize lazy binding for dynamic libraries during the linking process.
When a program uses shared libraries (dynamic libraries), such as .so files in Linux, the linking process involves resolving symbols (functions or global variables) from these libraries. Lazy binding delays the resolution of these symbols until they are actually referenced during the program’s execution, rather than resolving all symbols at startup.
Lazy binding delays the resolution of these symbols until they are actually referenced during the program’s execution, rather than resolving all symbols at startup.

-Wl,-z,now: When you compile a program using gcc with the -Wl,-z,now flag, it influences how the dynamic linker behaves at runtime, particularly when the program is executed and loaded into memory. This flag impacts the linking stage, ensuring that symbols from shared libraries are resolved and bound immediately during the linking phase.
During the binary’s execution, when shared libraries are loaded, immediate binding might help in reducing the overhead associated with symbol resolution at runtime because the symbols are already resolved and bound during the linking process.
In summary, the -Wl,-z,now flag influences the behavior of the linker while creating the binary, affecting how symbol resolution occurs when the binary is loaded and executed, potentially impacting the startup performance by pre-resolving symbols.

近期的一些杂项

infra decouple from kind of internal production system

重点并不是 infra 如何帮忙自动恢复, 只是略有提到; 重点还是训练的调参

famous uncorrectable ECC error

we just restart the run

try to make run stable (数学上的稳定)

FP16

Lost GPU
CUDA errors
Job hanging
NCCL error
Job Slowdown

High DRAM correctable errors etc.
blob storage issues

when we are training these models, we kind of just stare at tensorboard all day

in general the mixture of hardware issues, training like numerical converting issues

~30days change the hyperparameter to try to get through

56days, 53 - 54 restarts, OPT-175B survived 143K steps

Andrej Karpathy

LLM

LAMA-2-70B

fp16, 2bytes, 70B

2 * 70B = 140B bytes = 140 * 1,000,000,000 bytes = 140,000,000,000 bytes = 140 gigabytes (bytes, kbytes, mbytes, gbytes)

140GB

tokenize

encoder, 将字符串转换为整数编码
decoder, 将整数编码转为字符串

Terms

  • SXM: Server PCI Express Module, a high bandwidth socket solution for connecting Nvidia Compute Accelerators to a system
  • NVL: NVLink is a wire-based serial multi-lane near-range communications link developed by Nvidia. Unlike PCI Express, a device can consist of multiple NVLinks, and devices use mesh networking to communicate instead of a central hub.
  • PCIe: PCI Express (Peripheral Component Interconnect Express), officially abbreviated as PCIe or PCI-e,[1] is a high-speed serial computer expansion bus standard

from Wikipedia

H800 vs H100

  1. https://resources.nvidia.com/en-us-tensor-core/nvidia-tensor-core-gpu-datasheet

  2. NVIDIA H100 Tensor Core GPU

  3. H800 没找到 NVIDIA 官网 Specification, 只能从代理商和一些B站UP主看到的数据

H800 SXM H100 SXM
FP64 1 teraFLOPS 34 teraFLOPS
FP64 Tensor Core 1 teraFLOPS 67 teraFLOPS
FP32 67 teraFLOPS 67 teraFLOPS
TF32 Tensor Core 989 teraFLOPS 989 teraFLOPS
BFLOAT16 Tensor Core 1,979 teraFLOPS 1,979 teraFLOPS
FP16 Tensor Core 1,979 teraFLOPS 1,979 teraFLOPS
FP8 Tensor Core 3,958 teraFLOPS 3,958 teraFLOPS
INT8 Tensor Core 3,958 TOPS 3,958 TOPS
GPU memory 80GB 80GB
GPU memory bandwidth 3.35TB/s 3.35TB/s
Interconnect NVLink 400GB/s PCIe Gen5: 128GB/s NVLink 900GB/s PCIe Gen5: 128GB/s
  • H800 FP64 算力限制

Driver

https://resources.nvidia.com/en-us-tensor-core/gtc22-whitepaper-hopper

https://www.nvidia.com/content/dam/en-zz/Solutions/gtcs22/data-center/h100/PB-11133-001_v01.pdf

Software Specifications

Specification Description
Driver support Linux: R520 or later

CUDA

https://docs.nvidia.com/datacenter/tesla/drivers/index.html#cuda-arch-matrix

Architecture CUDA Capabilities First CUDA Toolkit Support
Hopper 9.0 CUDA 11.8
CUDA 12.0

TensorFlow

https://www.tensorflow.org/install/source#tested_build_configurations

Version Python version Compiler Build tools cuDNN CUDA
tensorflow-2.15.0 3.9-3.11 Clang 16.0.0 Bazel 6.1.0 8.8 12.2
tensorflow-2.14.0 3.9-3.11 Clang 16.0.0 Bazel 6.1.0 8.7 11.8
tensorflow-2.13.0 3.8-3.11 Clang 16.0.0 Bazel 5.3.0 8.6 11.8
tensorflow-2.12.0 3.8-3.11 GCC 9.3.1 Bazel 5.3.0 8.6 11.8
tensorflow-2.11.0 3.7-3.10 GCC 9.3.1 Bazel 5.3.0 8.1 11.2
tensorflow-2.6.0 3.6-3.9 GCC 7.3.1 Bazel 3.7.2 8.1 11.2

candidates on H800

  • >= tensorflow-2.12.0

docker images

1
2
3
docker pull tensorflow/tensorflow:2.14.0-gpu
docker pull tensorflow/tensorflow:2.13.0-gpu
docker pull tensorflow/tensorflow:2.12.0-gpu

PyTorch

https://pytorch.org/get-started/previous-versions/

Version CUDA
v1.13.1 11.6, 11.7
v2.0.0 11.7, 11.8
v2.0.1 11.7, 11.8
v2.1.0 11.8, 12.1
v2.1.1 11.8, 12.1

candidates on H800

  • >= v2.0.0, with cuda 11.8 support

docker images

1
2
3
4
5
docker pull pytorch/pytorch:2.1.0-cuda11.8-cudnn8-devel
docker pull pytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtime

docker pull pytorch/pytorch:2.0.1-cuda11.7-cudnn8-devel
docker pull pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime
0%