fix(rl): warmup window in plateau-decay LR controller fixes V cold-start
`alpha-rl-rzltn` exposed a bug in the plateau-decay design: V head's
`best` got bootstrapped to 7.12e-10 (machine epsilon) at step 1
because V regression had no reward signal yet — no trade had closed,
the bootstrap V target was 0, so the first V loss was effectively 0.
Every subsequent V loss EMA was orders of magnitude higher (4.07
at step 100, 1.15 at step 1000), so the improvement check
`loss_ema < best * 0.99` evaluated false FOREVER. The controller
then decayed lr_v every 1000 steps purely on the patience clock,
not because the model genuinely plateaued.
Cross-check across the 50k-step rzltn run:
* V best unique values: {0.0, 7.12e-10} — ONLY 2 across 50000 rows
* V best max: 7.12e-10
* V best-improvements: 0 (Q: 12, π: 12)
* V decays still fired: 7 (one every 1000 steps from step 1001)
The plateau-decay mechanics worked correctly — the controller counted
to 999 then halved LR exactly as designed. The bug was that "first
observation defines best forever" is degenerate for sparse-signal
heads whose first loss is a cold-start artifact.
## Fix: LR_WARMUP_STEPS
Three new ISV slots (one per head — Q, π, V at 436/437/438) hold a
monotonic warmup counter clamped at LR_WARMUP_STEPS = 500. During
warmup the controller:
* always overwrites `best` with current loss_ema (tracks the EMA
as it converges)
* holds the plateau counter at 0 (no decay fires during warmup)
* increments warmup_counter
Once warmup_counter >= LR_WARMUP_STEPS, the controller switches to
standard plateau detection — `best` then locks in at the
post-warmup loss_ema value (representative of the head's converged
loss scale), and patience counting begins.
At α=0.05 the EMA half-life is ~14 steps; 500 updates leaves ~35
half-lives, well past convergence. This gives V time to see its
first actual losses after trades start closing.
## Slot allocation
RL_SLOTS_END: 436 → 439 (adds 3 warmup counter slots).
## Wiring
* rl_lr_controller.cu — adds warmup_slot param to
plateau_decay_head, kernel takes 12
slot ints (was 9)
* isv_slots.rs — 3 new constants, RL_SLOTS_END += 3
* integrated.rs — launch_rl_lr_controller passes 12
slot ints
* alpha_rl_train.rs — diag JSONL emits new
lr_plateau.{head}.warmup field
## Verified gates (local sm_86)
G1 isv_bootstrap ✅
G3 controllers ✅
G4 target_update ✅
G6 r7d_per_wiring ✅
integrated_smoke ✅
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -67,9 +67,21 @@
|
|||||||
// 1000 steps ≈ 7 seconds at 145 steps/sec on L40S.
|
// 1000 steps ≈ 7 seconds at 145 steps/sec on L40S.
|
||||||
// DECAY_FACTOR: shrink LR by this on each plateau fire.
|
// DECAY_FACTOR: shrink LR by this on each plateau fire.
|
||||||
// 0.5 = "halve LR" (canonical ReduceLROnPlateau default).
|
// 0.5 = "halve LR" (canonical ReduceLROnPlateau default).
|
||||||
|
// LR_WARMUP_STEPS: number of EMA updates before `best` is locked in
|
||||||
|
// and plateau detection turns on. Without this, a head whose
|
||||||
|
// first non-zero loss observation is a cold-start artifact (e.g.
|
||||||
|
// V loss ≈ machine-epsilon at step 0 before any reward has
|
||||||
|
// flowed) pins `best` to an unreachable value, and the controller
|
||||||
|
// decays on the patience clock alone. At α=0.05 the EMA
|
||||||
|
// half-life is ≈14 steps; 500 updates leaves ≈35 half-lives,
|
||||||
|
// which is well past convergence. Canonical incident:
|
||||||
|
// alpha-rl-rzltn fold0 (commit 13d81dc5e) — V `best` stuck at
|
||||||
|
// 7.12e-10 across all 50k steps, only 2 unique best values
|
||||||
|
// across the entire run.
|
||||||
#define IMPROVEMENT_THRESHOLD 0.99f
|
#define IMPROVEMENT_THRESHOLD 0.99f
|
||||||
#define PLATEAU_PATIENCE 1000.0f
|
#define PLATEAU_PATIENCE 1000.0f
|
||||||
#define DECAY_FACTOR 0.5f
|
#define DECAY_FACTOR 0.5f
|
||||||
|
#define LR_WARMUP_STEPS 500.0f
|
||||||
|
|
||||||
__device__ __forceinline__ void plateau_decay_head(
|
__device__ __forceinline__ void plateau_decay_head(
|
||||||
float* isv,
|
float* isv,
|
||||||
@@ -77,7 +89,8 @@ __device__ __forceinline__ void plateau_decay_head(
|
|||||||
float observed_loss,
|
float observed_loss,
|
||||||
int loss_ema_slot,
|
int loss_ema_slot,
|
||||||
int best_slot,
|
int best_slot,
|
||||||
int counter_slot
|
int counter_slot,
|
||||||
|
int warmup_slot
|
||||||
) {
|
) {
|
||||||
const float lr_prev = isv[lr_idx];
|
const float lr_prev = isv[lr_idx];
|
||||||
|
|
||||||
@@ -109,16 +122,23 @@ __device__ __forceinline__ void plateau_decay_head(
|
|||||||
}
|
}
|
||||||
isv[loss_ema_slot] = loss_ema_new;
|
isv[loss_ema_slot] = loss_ema_new;
|
||||||
|
|
||||||
// ── 2. Compare to best — improvement or plateau? ────────────────
|
// ── 2. Warmup: unconditionally overwrite best while EMA settles.
|
||||||
const float best_prev = isv[best_slot];
|
//
|
||||||
|
// During the first LR_WARMUP_STEPS observations, `best` tracks
|
||||||
if (best_prev == 0.0f) {
|
// the current loss_ema. Without this, sparse-signal heads (V
|
||||||
// First-observation bootstrap for best (and counter).
|
// before any trade closes) lock `best` to a near-zero first
|
||||||
|
// observation that subsequent EMA values can never beat.
|
||||||
|
const float warmup_prev = isv[warmup_slot];
|
||||||
|
if (warmup_prev < LR_WARMUP_STEPS) {
|
||||||
isv[best_slot] = loss_ema_new;
|
isv[best_slot] = loss_ema_new;
|
||||||
isv[counter_slot] = 0.0f;
|
isv[counter_slot] = 0.0f;
|
||||||
|
isv[warmup_slot] = warmup_prev + 1.0f;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 3. Compare to best — improvement or plateau? ────────────────
|
||||||
|
const float best_prev = isv[best_slot];
|
||||||
|
|
||||||
if (loss_ema_new < best_prev * IMPROVEMENT_THRESHOLD) {
|
if (loss_ema_new < best_prev * IMPROVEMENT_THRESHOLD) {
|
||||||
// Improvement — update best, reset counter.
|
// Improvement — update best, reset counter.
|
||||||
isv[best_slot] = loss_ema_new;
|
isv[best_slot] = loss_ema_new;
|
||||||
@@ -126,7 +146,7 @@ __device__ __forceinline__ void plateau_decay_head(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 3. Plateau — increment counter; maybe decay LR. ─────────────
|
// ── 4. Plateau — increment counter; maybe decay LR. ─────────────
|
||||||
const float counter_next = isv[counter_slot] + 1.0f;
|
const float counter_next = isv[counter_slot] + 1.0f;
|
||||||
if (counter_next >= PLATEAU_PATIENCE) {
|
if (counter_next >= PLATEAU_PATIENCE) {
|
||||||
// Decay fires. Halve LR (clamped to MIN), reset counter,
|
// Decay fires. Halve LR (clamped to MIN), reset counter,
|
||||||
@@ -150,12 +170,15 @@ extern "C" __global__ void rl_lr_controller(
|
|||||||
int q_loss_ema_slot,
|
int q_loss_ema_slot,
|
||||||
int q_best_slot,
|
int q_best_slot,
|
||||||
int q_counter_slot,
|
int q_counter_slot,
|
||||||
|
int q_warmup_slot,
|
||||||
int pi_loss_ema_slot,
|
int pi_loss_ema_slot,
|
||||||
int pi_best_slot,
|
int pi_best_slot,
|
||||||
int pi_counter_slot,
|
int pi_counter_slot,
|
||||||
|
int pi_warmup_slot,
|
||||||
int v_loss_ema_slot,
|
int v_loss_ema_slot,
|
||||||
int v_best_slot,
|
int v_best_slot,
|
||||||
int v_counter_slot
|
int v_counter_slot,
|
||||||
|
int v_warmup_slot
|
||||||
) {
|
) {
|
||||||
if (threadIdx.x != 0 || blockIdx.x != 0) return;
|
if (threadIdx.x != 0 || blockIdx.x != 0) return;
|
||||||
|
|
||||||
@@ -163,12 +186,12 @@ extern "C" __global__ void rl_lr_controller(
|
|||||||
// trainer doesn't compute per-step loss observations for them.
|
// trainer doesn't compute per-step loss observations for them.
|
||||||
// Pass loss_ema_slot=-1 to the plateau function which then
|
// Pass loss_ema_slot=-1 to the plateau function which then
|
||||||
// early-returns, holding lr_bce / lr_aux at LR_BOOTSTRAP.
|
// early-returns, holding lr_bce / lr_aux at LR_BOOTSTRAP.
|
||||||
plateau_decay_head(isv, RL_LR_BCE_INDEX, observed_loss_bce, -1, -1, -1);
|
plateau_decay_head(isv, RL_LR_BCE_INDEX, observed_loss_bce, -1, -1, -1, -1);
|
||||||
plateau_decay_head(isv, RL_LR_Q_INDEX, observed_loss_q,
|
plateau_decay_head(isv, RL_LR_Q_INDEX, observed_loss_q,
|
||||||
q_loss_ema_slot, q_best_slot, q_counter_slot);
|
q_loss_ema_slot, q_best_slot, q_counter_slot, q_warmup_slot);
|
||||||
plateau_decay_head(isv, RL_LR_PI_INDEX, observed_loss_pi,
|
plateau_decay_head(isv, RL_LR_PI_INDEX, observed_loss_pi,
|
||||||
pi_loss_ema_slot, pi_best_slot, pi_counter_slot);
|
pi_loss_ema_slot, pi_best_slot, pi_counter_slot, pi_warmup_slot);
|
||||||
plateau_decay_head(isv, RL_LR_V_INDEX, observed_loss_v,
|
plateau_decay_head(isv, RL_LR_V_INDEX, observed_loss_v,
|
||||||
v_loss_ema_slot, v_best_slot, v_counter_slot);
|
v_loss_ema_slot, v_best_slot, v_counter_slot, v_warmup_slot);
|
||||||
plateau_decay_head(isv, RL_LR_AUX_INDEX, observed_loss_aux, -1, -1, -1);
|
plateau_decay_head(isv, RL_LR_AUX_INDEX, observed_loss_aux, -1, -1, -1, -1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,9 +41,10 @@ use ml_alpha::rl::isv_slots::{
|
|||||||
RL_ADVANTAGE_VAR_RATIO_EMA_INDEX, RL_ENTROPY_COEF_INDEX, RL_ENTROPY_OBSERVED_EMA_INDEX,
|
RL_ADVANTAGE_VAR_RATIO_EMA_INDEX, RL_ENTROPY_COEF_INDEX, RL_ENTROPY_OBSERVED_EMA_INDEX,
|
||||||
RL_GAMMA_INDEX, RL_KL_PI_EMA_INDEX, RL_LR_AUX_INDEX, RL_LR_BCE_INDEX, RL_LR_PI_BEST_LOSS_INDEX,
|
RL_GAMMA_INDEX, RL_KL_PI_EMA_INDEX, RL_LR_AUX_INDEX, RL_LR_BCE_INDEX, RL_LR_PI_BEST_LOSS_INDEX,
|
||||||
RL_LR_PI_INDEX, RL_LR_PI_LOSS_EMA_INDEX, RL_LR_PI_STEPS_SINCE_BEST_INDEX,
|
RL_LR_PI_INDEX, RL_LR_PI_LOSS_EMA_INDEX, RL_LR_PI_STEPS_SINCE_BEST_INDEX,
|
||||||
RL_LR_Q_BEST_LOSS_INDEX, RL_LR_Q_INDEX, RL_LR_Q_LOSS_EMA_INDEX,
|
RL_LR_PI_WARMUP_COUNTER_INDEX, RL_LR_Q_BEST_LOSS_INDEX, RL_LR_Q_INDEX,
|
||||||
RL_LR_Q_STEPS_SINCE_BEST_INDEX, RL_LR_V_BEST_LOSS_INDEX, RL_LR_V_INDEX,
|
RL_LR_Q_LOSS_EMA_INDEX, RL_LR_Q_STEPS_SINCE_BEST_INDEX, RL_LR_Q_WARMUP_COUNTER_INDEX,
|
||||||
RL_LR_V_LOSS_EMA_INDEX, RL_LR_V_STEPS_SINCE_BEST_INDEX, RL_MEAN_ABS_PNL_EMA_INDEX,
|
RL_LR_V_BEST_LOSS_INDEX, RL_LR_V_INDEX, RL_LR_V_LOSS_EMA_INDEX,
|
||||||
|
RL_LR_V_STEPS_SINCE_BEST_INDEX, RL_LR_V_WARMUP_COUNTER_INDEX, RL_MEAN_ABS_PNL_EMA_INDEX,
|
||||||
RL_MEAN_TRADE_DURATION_EMA_INDEX, RL_N_ROLLOUT_STEPS_INDEX, RL_PER_ALPHA_INDEX,
|
RL_MEAN_TRADE_DURATION_EMA_INDEX, RL_N_ROLLOUT_STEPS_INDEX, RL_PER_ALPHA_INDEX,
|
||||||
RL_PI_GRAD_NORM_EMA_INDEX, RL_PPO_CLIP_INDEX, RL_Q_DIVERGENCE_EMA_INDEX,
|
RL_PI_GRAD_NORM_EMA_INDEX, RL_PPO_CLIP_INDEX, RL_Q_DIVERGENCE_EMA_INDEX,
|
||||||
RL_Q_GRAD_NORM_EMA_INDEX, RL_REWARD_SCALE_INDEX, RL_TARGET_TAU_INDEX, RL_TD_KURTOSIS_EMA_INDEX,
|
RL_Q_GRAD_NORM_EMA_INDEX, RL_REWARD_SCALE_INDEX, RL_TARGET_TAU_INDEX, RL_TD_KURTOSIS_EMA_INDEX,
|
||||||
@@ -528,13 +529,16 @@ fn main() -> Result<()> {
|
|||||||
"lr_plateau": {
|
"lr_plateau": {
|
||||||
"q": { "loss_ema": isv[RL_LR_Q_LOSS_EMA_INDEX],
|
"q": { "loss_ema": isv[RL_LR_Q_LOSS_EMA_INDEX],
|
||||||
"best": isv[RL_LR_Q_BEST_LOSS_INDEX],
|
"best": isv[RL_LR_Q_BEST_LOSS_INDEX],
|
||||||
"stale": isv[RL_LR_Q_STEPS_SINCE_BEST_INDEX] },
|
"stale": isv[RL_LR_Q_STEPS_SINCE_BEST_INDEX],
|
||||||
|
"warmup": isv[RL_LR_Q_WARMUP_COUNTER_INDEX] },
|
||||||
"pi": { "loss_ema": isv[RL_LR_PI_LOSS_EMA_INDEX],
|
"pi": { "loss_ema": isv[RL_LR_PI_LOSS_EMA_INDEX],
|
||||||
"best": isv[RL_LR_PI_BEST_LOSS_INDEX],
|
"best": isv[RL_LR_PI_BEST_LOSS_INDEX],
|
||||||
"stale": isv[RL_LR_PI_STEPS_SINCE_BEST_INDEX] },
|
"stale": isv[RL_LR_PI_STEPS_SINCE_BEST_INDEX],
|
||||||
|
"warmup": isv[RL_LR_PI_WARMUP_COUNTER_INDEX] },
|
||||||
"v": { "loss_ema": isv[RL_LR_V_LOSS_EMA_INDEX],
|
"v": { "loss_ema": isv[RL_LR_V_LOSS_EMA_INDEX],
|
||||||
"best": isv[RL_LR_V_BEST_LOSS_INDEX],
|
"best": isv[RL_LR_V_BEST_LOSS_INDEX],
|
||||||
"stale": isv[RL_LR_V_STEPS_SINCE_BEST_INDEX] },
|
"stale": isv[RL_LR_V_STEPS_SINCE_BEST_INDEX],
|
||||||
|
"warmup": isv[RL_LR_V_WARMUP_COUNTER_INDEX] },
|
||||||
},
|
},
|
||||||
"replay_len": trainer.replay.len(),
|
"replay_len": trainer.replay.len(),
|
||||||
"rewards": {
|
"rewards": {
|
||||||
|
|||||||
@@ -175,6 +175,26 @@ pub const RL_LR_V_LOSS_EMA_INDEX: usize = 433;
|
|||||||
pub const RL_LR_V_BEST_LOSS_INDEX: usize = 434;
|
pub const RL_LR_V_BEST_LOSS_INDEX: usize = 434;
|
||||||
pub const RL_LR_V_STEPS_SINCE_BEST_INDEX: usize = 435;
|
pub const RL_LR_V_STEPS_SINCE_BEST_INDEX: usize = 435;
|
||||||
|
|
||||||
|
// Warmup counters for the plateau-decay controller. Without these,
|
||||||
|
// a head whose first non-zero loss observation is a cold-start
|
||||||
|
// artifact (e.g. V loss = 7e-10 at step 0 before any reward has
|
||||||
|
// flowed) pins `best` to that degenerate value, and the improvement
|
||||||
|
// check `loss_ema < best * 0.99` evaluates false forever — the
|
||||||
|
// controller then decays the LR purely on the patience clock,
|
||||||
|
// not because the model genuinely plateaued. The warmup window
|
||||||
|
// unconditionally overwrites `best` with the current loss EMA for
|
||||||
|
// the first `LR_WARMUP_STEPS` observations, locking in a
|
||||||
|
// representative reference once the EMA has had time to converge
|
||||||
|
// past cold-start. Canonical incident: alpha-rl-rzltn (commit
|
||||||
|
// 13d81dc5e) — V head best stuck at 7.12e-10 across all 50k steps,
|
||||||
|
// only 2 unique best values across the entire run.
|
||||||
|
//
|
||||||
|
// Stored as f32 (mantissa precision to 16M — well beyond
|
||||||
|
// LR_WARMUP_STEPS).
|
||||||
|
pub const RL_LR_Q_WARMUP_COUNTER_INDEX: usize = 436;
|
||||||
|
pub const RL_LR_PI_WARMUP_COUNTER_INDEX: usize = 437;
|
||||||
|
pub const RL_LR_V_WARMUP_COUNTER_INDEX: usize = 438;
|
||||||
|
|
||||||
/// Last RL-allocated slot index (exclusive). The integrated trainer
|
/// Last RL-allocated slot index (exclusive). The integrated trainer
|
||||||
/// extends `ISV_TOTAL_DIM` to at least this value at trainer init time.
|
/// extends `ISV_TOTAL_DIM` to at least this value at trainer init time.
|
||||||
pub const RL_SLOTS_END: usize = 436;
|
pub const RL_SLOTS_END: usize = 439;
|
||||||
|
|||||||
@@ -2946,7 +2946,7 @@ impl IntegratedTrainer {
|
|||||||
///
|
///
|
||||||
/// Loss observations are passed as scalar floats (host-side
|
/// Loss observations are passed as scalar floats (host-side
|
||||||
/// values from the trainer's last step). Per-head state lives
|
/// values from the trainer's last step). Per-head state lives
|
||||||
/// in ISV slots 427..436 (loss_ema, best, counter ×3).
|
/// in ISV slots 427..439 (loss_ema, best, counter, warmup ×3).
|
||||||
fn launch_rl_lr_controller(
|
fn launch_rl_lr_controller(
|
||||||
&self,
|
&self,
|
||||||
observed_loss_q: f32,
|
observed_loss_q: f32,
|
||||||
@@ -2966,18 +2966,24 @@ impl IntegratedTrainer {
|
|||||||
crate::rl::isv_slots::RL_LR_Q_BEST_LOSS_INDEX as i32;
|
crate::rl::isv_slots::RL_LR_Q_BEST_LOSS_INDEX as i32;
|
||||||
let q_counter_slot: i32 =
|
let q_counter_slot: i32 =
|
||||||
crate::rl::isv_slots::RL_LR_Q_STEPS_SINCE_BEST_INDEX as i32;
|
crate::rl::isv_slots::RL_LR_Q_STEPS_SINCE_BEST_INDEX as i32;
|
||||||
|
let q_warmup_slot: i32 =
|
||||||
|
crate::rl::isv_slots::RL_LR_Q_WARMUP_COUNTER_INDEX as i32;
|
||||||
let pi_loss_ema_slot: i32 =
|
let pi_loss_ema_slot: i32 =
|
||||||
crate::rl::isv_slots::RL_LR_PI_LOSS_EMA_INDEX as i32;
|
crate::rl::isv_slots::RL_LR_PI_LOSS_EMA_INDEX as i32;
|
||||||
let pi_best_slot: i32 =
|
let pi_best_slot: i32 =
|
||||||
crate::rl::isv_slots::RL_LR_PI_BEST_LOSS_INDEX as i32;
|
crate::rl::isv_slots::RL_LR_PI_BEST_LOSS_INDEX as i32;
|
||||||
let pi_counter_slot: i32 =
|
let pi_counter_slot: i32 =
|
||||||
crate::rl::isv_slots::RL_LR_PI_STEPS_SINCE_BEST_INDEX as i32;
|
crate::rl::isv_slots::RL_LR_PI_STEPS_SINCE_BEST_INDEX as i32;
|
||||||
|
let pi_warmup_slot: i32 =
|
||||||
|
crate::rl::isv_slots::RL_LR_PI_WARMUP_COUNTER_INDEX as i32;
|
||||||
let v_loss_ema_slot: i32 =
|
let v_loss_ema_slot: i32 =
|
||||||
crate::rl::isv_slots::RL_LR_V_LOSS_EMA_INDEX as i32;
|
crate::rl::isv_slots::RL_LR_V_LOSS_EMA_INDEX as i32;
|
||||||
let v_best_slot: i32 =
|
let v_best_slot: i32 =
|
||||||
crate::rl::isv_slots::RL_LR_V_BEST_LOSS_INDEX as i32;
|
crate::rl::isv_slots::RL_LR_V_BEST_LOSS_INDEX as i32;
|
||||||
let v_counter_slot: i32 =
|
let v_counter_slot: i32 =
|
||||||
crate::rl::isv_slots::RL_LR_V_STEPS_SINCE_BEST_INDEX as i32;
|
crate::rl::isv_slots::RL_LR_V_STEPS_SINCE_BEST_INDEX as i32;
|
||||||
|
let v_warmup_slot: i32 =
|
||||||
|
crate::rl::isv_slots::RL_LR_V_WARMUP_COUNTER_INDEX as i32;
|
||||||
let mut launch = self.stream.launch_builder(&self.rl_lr_controller_fn);
|
let mut launch = self.stream.launch_builder(&self.rl_lr_controller_fn);
|
||||||
launch
|
launch
|
||||||
.arg(&self.isv_d)
|
.arg(&self.isv_d)
|
||||||
@@ -2989,12 +2995,15 @@ impl IntegratedTrainer {
|
|||||||
.arg(&q_loss_ema_slot)
|
.arg(&q_loss_ema_slot)
|
||||||
.arg(&q_best_slot)
|
.arg(&q_best_slot)
|
||||||
.arg(&q_counter_slot)
|
.arg(&q_counter_slot)
|
||||||
|
.arg(&q_warmup_slot)
|
||||||
.arg(&pi_loss_ema_slot)
|
.arg(&pi_loss_ema_slot)
|
||||||
.arg(&pi_best_slot)
|
.arg(&pi_best_slot)
|
||||||
.arg(&pi_counter_slot)
|
.arg(&pi_counter_slot)
|
||||||
|
.arg(&pi_warmup_slot)
|
||||||
.arg(&v_loss_ema_slot)
|
.arg(&v_loss_ema_slot)
|
||||||
.arg(&v_best_slot)
|
.arg(&v_best_slot)
|
||||||
.arg(&v_counter_slot);
|
.arg(&v_counter_slot)
|
||||||
|
.arg(&v_warmup_slot);
|
||||||
unsafe {
|
unsafe {
|
||||||
launch.launch(cfg).context("rl_lr_controller launch")?;
|
launch.launch(cfg).context("rl_lr_controller launch")?;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user