feat(rl): wire IQN into trainer — forward alongside C51 each step
IQN head now runs in parallel with C51 on every step: - Constructs IqnHead in IntegratedTrainer::new() - Per-step: samples tau ~ U(0,1), forwards h_t through IQN, computes expected Q via tau-mean reduction - Per-replay: forwards sampled_h_t through IQN (verifying the head runs on replay data) - Adam optimizers allocated for 4 IQN weight tensors Not yet wired: IQN loss/backward, target soft update, ensemble action selection. IQN runs forward-only — its Q values are computed but not yet consumed for action decisions or gradient. Local smoke: 100 steps, no crash, l_q=2.30 with both heads active. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -343,6 +343,12 @@ pub struct IntegratedTrainer {
|
||||
/// DQN distributional Q-head (Phase C).
|
||||
pub dqn_head: DqnHead,
|
||||
|
||||
/// IQN distributional Q-head — runs alongside C51 for ensemble
|
||||
/// action selection. Forward produces `Q(s, τ, a)` for N_TAU
|
||||
/// quantile samples; expected Q is combined with C51's E[Q] via
|
||||
/// ISV[RL_IQN_ENSEMBLE_ALPHA_INDEX].
|
||||
pub iqn_head: crate::rl::iqn::IqnHead,
|
||||
|
||||
/// PPO policy head (Phase D).
|
||||
pub policy_head: PolicyHead,
|
||||
|
||||
@@ -354,6 +360,11 @@ pub struct IntegratedTrainer {
|
||||
/// `AdamW` wrapper; each instance owns its own m / v / step counter.
|
||||
pub dqn_w_adam: AdamW,
|
||||
pub dqn_b_adam: AdamW,
|
||||
/// IQN head Adam optimisers — w_embed/b_embed/w_out/b_out.
|
||||
pub iqn_w_embed_adam: AdamW,
|
||||
pub iqn_b_embed_adam: AdamW,
|
||||
pub iqn_w_out_adam: AdamW,
|
||||
pub iqn_b_out_adam: AdamW,
|
||||
pub policy_w_adam: AdamW,
|
||||
pub policy_b_adam: AdamW,
|
||||
pub value_w_adam: AdamW,
|
||||
@@ -634,6 +645,17 @@ pub struct IntegratedTrainer {
|
||||
/// h_t-as-proxy comment at the call site).
|
||||
pub h_tp1_d: CudaSlice<f32>,
|
||||
|
||||
// ── IQN per-step device buffers ──────────────────────────────────
|
||||
/// Per-step quantile fractions `[B × N_TAU]` sampled from U(0,1).
|
||||
/// Populated each step from the host RNG via mapped-pinned upload.
|
||||
pub iqn_tau_d: CudaSlice<f32>,
|
||||
/// IQN forward output `[B × N_TAU × N_ACTIONS]`.
|
||||
pub iqn_q_values_d: CudaSlice<f32>,
|
||||
/// IQN expected Q `[B × N_ACTIONS]` = mean over tau dimension.
|
||||
pub iqn_expected_q_d: CudaSlice<f32>,
|
||||
/// Host RNG for tau sampling (deterministic from dqn_seed + offset).
|
||||
iqn_tau_rng: rand_chacha::ChaCha8Rng,
|
||||
|
||||
// ── Phase R7d: PER + off-policy Q replay ──────────────────────────
|
||||
/// Prioritised Experience Replay buffer (off-policy DQN training).
|
||||
/// Per `feedback_always_per` PER is always-on. Wired in R7d after
|
||||
@@ -758,6 +780,16 @@ impl IntegratedTrainer {
|
||||
)
|
||||
.context("DqnHead::new")?;
|
||||
|
||||
let iqn_head = crate::rl::iqn::IqnHead::new(
|
||||
dev,
|
||||
crate::rl::iqn::IqnHeadConfig {
|
||||
hidden_dim: HIDDEN_DIM,
|
||||
seed: cfg.dqn_seed.wrapping_add(100),
|
||||
n_tau: 32,
|
||||
},
|
||||
)
|
||||
.context("IqnHead::new")?;
|
||||
|
||||
let policy_head = PolicyHead::new(
|
||||
dev,
|
||||
PpoHeadsConfig {
|
||||
@@ -792,6 +824,14 @@ impl IntegratedTrainer {
|
||||
AdamW::new(dev, dqn_head.w_d.len(), lr_placeholder).context("dqn_w_adam")?;
|
||||
let dqn_b_adam =
|
||||
AdamW::new(dev, dqn_head.b_d.len(), lr_placeholder).context("dqn_b_adam")?;
|
||||
let iqn_w_embed_adam =
|
||||
AdamW::new(dev, iqn_head.w_embed_d.len(), lr_placeholder).context("iqn_w_embed_adam")?;
|
||||
let iqn_b_embed_adam =
|
||||
AdamW::new(dev, iqn_head.b_embed_d.len(), lr_placeholder).context("iqn_b_embed_adam")?;
|
||||
let iqn_w_out_adam =
|
||||
AdamW::new(dev, iqn_head.w_out_d.len(), lr_placeholder).context("iqn_w_out_adam")?;
|
||||
let iqn_b_out_adam =
|
||||
AdamW::new(dev, iqn_head.b_out_d.len(), lr_placeholder).context("iqn_b_out_adam")?;
|
||||
let policy_w_adam =
|
||||
AdamW::new(dev, policy_head.w_d.len(), lr_placeholder).context("policy_w_adam")?;
|
||||
let policy_b_adam =
|
||||
@@ -1317,6 +1357,22 @@ impl IntegratedTrainer {
|
||||
.alloc_zeros::<f32>(b_size * HIDDEN_DIM)
|
||||
.context("alloc h_tp1_d")?;
|
||||
|
||||
// IQN per-step buffers: tau samples, Q(s,τ,a), E[Q(s,a)].
|
||||
let iqn_n_tau = iqn_head.n_tau();
|
||||
let iqn_tau_d = stream
|
||||
.alloc_zeros::<f32>(b_size * iqn_n_tau)
|
||||
.context("alloc iqn_tau_d")?;
|
||||
let iqn_q_values_d = stream
|
||||
.alloc_zeros::<f32>(b_size * iqn_n_tau * N_ACTIONS)
|
||||
.context("alloc iqn_q_values_d")?;
|
||||
let iqn_expected_q_d = stream
|
||||
.alloc_zeros::<f32>(b_size * N_ACTIONS)
|
||||
.context("alloc iqn_expected_q_d")?;
|
||||
let iqn_tau_rng =
|
||||
<rand_chacha::ChaCha8Rng as SeedableRng>::seed_from_u64(
|
||||
cfg.dqn_seed.wrapping_add(0x1C_A0_7A_00),
|
||||
);
|
||||
|
||||
// Phase R7d: PER buffer + sampled-batch device scratch.
|
||||
// The replay buffer holds per-transition device payloads
|
||||
// (h_t / next_h_t as small `CudaSlice<f32>(HIDDEN_DIM)` slices
|
||||
@@ -1391,10 +1447,15 @@ impl IntegratedTrainer {
|
||||
cfg,
|
||||
perception,
|
||||
dqn_head,
|
||||
iqn_head,
|
||||
policy_head,
|
||||
value_head,
|
||||
dqn_w_adam,
|
||||
dqn_b_adam,
|
||||
iqn_w_embed_adam,
|
||||
iqn_b_embed_adam,
|
||||
iqn_w_out_adam,
|
||||
iqn_b_out_adam,
|
||||
policy_w_adam,
|
||||
policy_b_adam,
|
||||
value_w_adam,
|
||||
@@ -1534,6 +1595,10 @@ impl IntegratedTrainer {
|
||||
advantages_d,
|
||||
returns_d,
|
||||
h_tp1_d,
|
||||
iqn_tau_d,
|
||||
iqn_q_values_d,
|
||||
iqn_expected_q_d,
|
||||
iqn_tau_rng,
|
||||
replay,
|
||||
n_step_buffer: (0..b_size).map(|_| Vec::with_capacity(16)).collect(),
|
||||
sampled_h_t_d,
|
||||
@@ -3464,6 +3529,36 @@ impl IntegratedTrainer {
|
||||
.forward(&self.sampled_h_tp1_d, b_size, &mut q_logits_tp1_sampled_d)
|
||||
.context("dqn_replay_step: dqn_head.forward(sampled_h_tp1)")?;
|
||||
|
||||
// ── 1b. IQN forward on sampled_h_t (forward-only, loss in
|
||||
// follow-up). Verifies the head runs on replay data without
|
||||
// crashing; the expected_q output can feed an ensemble Q later.
|
||||
{
|
||||
let n_tau = self.iqn_head.n_tau();
|
||||
let tau_len = b_size * n_tau;
|
||||
let tau_host: Vec<f32> = (0..tau_len)
|
||||
.map(|_| self.iqn_tau_rng.gen::<f32>())
|
||||
.collect();
|
||||
write_slice_f32_d(&self.stream, &tau_host, &mut self.iqn_tau_d)
|
||||
.context("dqn_replay_step: iqn_tau upload")?;
|
||||
self.iqn_head
|
||||
.forward(
|
||||
&self.sampled_h_t_d,
|
||||
&self.iqn_tau_d,
|
||||
b_size,
|
||||
n_tau,
|
||||
&mut self.iqn_q_values_d,
|
||||
)
|
||||
.context("dqn_replay_step: iqn_head.forward(sampled_h_t)")?;
|
||||
self.iqn_head
|
||||
.expected_q(
|
||||
&self.iqn_q_values_d,
|
||||
b_size,
|
||||
n_tau,
|
||||
&mut self.iqn_expected_q_d,
|
||||
)
|
||||
.context("dqn_replay_step: iqn_head.expected_q")?;
|
||||
}
|
||||
|
||||
// ── 2. Double-DQN argmax on online Q at h_tp1 ───────────────
|
||||
{
|
||||
let cfg_argmax = LaunchConfig {
|
||||
@@ -3718,6 +3813,37 @@ impl IntegratedTrainer {
|
||||
.forward(&self.h_tp1_d, &self.isv_d, b_size, &mut v_pred_tp1_d)
|
||||
.context("step_with_lobsim: value_head.forward(h_tp1) for true V(s_{t+1})")?;
|
||||
|
||||
// ── IQN forward alongside C51 ────────────────────────────────
|
||||
// Sample tau ~ U(0,1) on host, upload via mapped-pinned, then
|
||||
// run forward + expected_q reduction. The expected Q output is
|
||||
// available for the Q→π distillation ensemble in a follow-up.
|
||||
{
|
||||
let n_tau = self.iqn_head.n_tau();
|
||||
let tau_len = b_size * n_tau;
|
||||
let tau_host: Vec<f32> = (0..tau_len)
|
||||
.map(|_| self.iqn_tau_rng.gen::<f32>())
|
||||
.collect();
|
||||
write_slice_f32_d(&self.stream, &tau_host, &mut self.iqn_tau_d)
|
||||
.context("step_with_lobsim: iqn_tau upload")?;
|
||||
self.iqn_head
|
||||
.forward(
|
||||
h_t_borrow,
|
||||
&self.iqn_tau_d,
|
||||
b_size,
|
||||
n_tau,
|
||||
&mut self.iqn_q_values_d,
|
||||
)
|
||||
.context("step_with_lobsim: iqn_head.forward(h_t)")?;
|
||||
self.iqn_head
|
||||
.expected_q(
|
||||
&self.iqn_q_values_d,
|
||||
b_size,
|
||||
n_tau,
|
||||
&mut self.iqn_expected_q_d,
|
||||
)
|
||||
.context("step_with_lobsim: iqn_head.expected_q")?;
|
||||
}
|
||||
|
||||
// ── Step 2b: Forward π logits for log_pi_old. ─────────────────
|
||||
let mut pi_logits_d = self.stream.alloc_zeros::<f32>(b_size * N_ACTIONS)?;
|
||||
self.policy_head
|
||||
|
||||
Reference in New Issue
Block a user