Files
foxhunt/crates/ml-alpha/cuda/rl_q_distill_lambda_controller.cu
jgrusewski 96abc364b5 fix(rl): asymmetric Q→π distillation λ controller — ramp fast, decay slow
Root cause of q_pi_agree collapse: the symmetric Schulman controller
decayed λ from 0.5 to 0.001 in 34 steps (÷1.2/step), killing Q→π
coupling. Once decoupled, Q and π learned independent policies,
making q_pi_agree drop to 1e-22.

Three fixes:
1. MIN_LAMBDA 0.001 → 0.05: Q pull never drops below 5% strength
2. Asymmetric rates: ramp 1.2×/step (4 steps to double), decay
   0.998×/step (347 steps to halve). Coupling establishes fast and
   persists for thousands of steps.
3. Dead zone tolerance 1.5× → 3.0×: natural KL fluctuation stays
   in-band instead of triggering constant ramp/decay cycles.

Seed λ raised to 0.1 so distillation is active from step 0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 11:54:17 +02:00

54 lines
2.1 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// rl_q_distill_lambda_controller.cu — adaptive λ_distill controller.
//
// Adapts the Q→π distillation strength (λ, slot 486) from the KL
// divergence between Q's Boltzmann policy and π's softmax (slot 488).
//
// Controller logic (asymmetric bounded step):
//
// if KL > target × TOLERANCE → λ *= RAMP_RATE (Q signal not
// landing, pull harder)
// if KL < target / TOLERANCE → λ *= DECAY_RATE (aligned, ease off)
// else → λ unchanged (dead-zone)
//
// CRITICAL DESIGN: decay is 100× slower than ramp. This prevents the
// self-defeating oscillation where λ ramps up → Q and π align → λ
// collapses → they diverge again. The asymmetry means coupling
// establishes quickly but persists for thousands of steps, giving
// Q time to teach π meaningful action preferences.
//
// Per `feedback_no_atomicadd`: single-thread kernel, no atomics.
// Per `feedback_cpu_is_read_only`: all state in ISV.
#define RL_Q_DISTILL_LAMBDA_INDEX 486
#define RL_Q_DISTILL_KL_EMA_INDEX 488
#define RL_Q_DISTILL_KL_TARGET_INDEX 491
#define MIN_LAMBDA 0.05f
#define MAX_LAMBDA 1.0f
#define KL_TOLERANCE 3.0f
#define LAMBDA_RAMP_RATE 1.2f
#define LAMBDA_DECAY_RATE 0.998f
extern "C" __global__ void rl_q_distill_lambda_controller(
float* __restrict__ isv
) {
if (threadIdx.x != 0 || blockIdx.x != 0) return;
const float kl_observed = isv[RL_Q_DISTILL_KL_EMA_INDEX];
const float kl_target = isv[RL_Q_DISTILL_KL_TARGET_INDEX];
float lambda = isv[RL_Q_DISTILL_LAMBDA_INDEX];
if (kl_observed <= 0.0f || kl_target <= 0.0f || lambda <= 0.0f) return;
const float upper = kl_target * KL_TOLERANCE;
const float lower = kl_target / KL_TOLERANCE;
if (kl_observed > upper) {
lambda = fminf(MAX_LAMBDA, lambda * LAMBDA_RAMP_RATE);
} else if (kl_observed < lower) {
lambda = fmaxf(MIN_LAMBDA, lambda * LAMBDA_DECAY_RATE);
}
isv[RL_Q_DISTILL_LAMBDA_INDEX] = lambda;
}