From 719d3b236793ca5dfdea372c7fbb5efdc8f1f4fd Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Thu, 26 Mar 2026 08:25:38 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20revert=20c51/mse=20to=20runtime=20NVRTC?= =?UTF-8?q?=20=E2=80=94=20NUM=5FATOMS=20controls=20loop=20bounds,=20not=20?= =?UTF-8?q?just=20sizing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit c51_loss and mse_loss kernels use NUM_ATOMS for loop bounds AND shared memory. Precompiling with NUM_ATOMS=51 then running with num_atoms=101 causes wrong loop iterations. These 2 kernels MUST use runtime #define injection to match the hyperopt config. 35 of 37 kernels remain precompiled. Only c51_loss and mse_loss use runtime NVRTC (they're the only kernels where a #define controls logic). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../dqn/20260326_072108/best_params.json | 71 +++++++++++++++++++ .../dqn/20260326_072108/campaign_summary.json | 7 ++ .../ml/src/cuda_pipeline/gpu_dqn_trainer.rs | 49 +++++++++++-- 3 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 crates/ml/ml/hyperopt_results/dqn/20260326_072108/best_params.json create mode 100644 crates/ml/ml/hyperopt_results/dqn/20260326_072108/campaign_summary.json diff --git a/crates/ml/ml/hyperopt_results/dqn/20260326_072108/best_params.json b/crates/ml/ml/hyperopt_results/dqn/20260326_072108/best_params.json new file mode 100644 index 000000000..971ef1ac6 --- /dev/null +++ b/crates/ml/ml/hyperopt_results/dqn/20260326_072108/best_params.json @@ -0,0 +1,71 @@ +{ + "learning_rate": 0.00005994999354431849, + "batch_size": 307, + "gamma": 0.9572818589229506, + "buffer_size": 66246, + "max_position_absolute": 1.1030284538648683, + "huber_delta": 17.775790174607682, + "entropy_coefficient": 0.38184099247597697, + "transaction_cost_multiplier": 1.7738774066741243, + "per_alpha": 0.45251155566697393, + "per_beta_start": 0.20130083854118397, + "dueling_hidden_dim": 128, + "n_steps": 4, + "tau": 0.008986246204051671, + "num_atoms": 101, + "v_min": -280.91110000212706, + "v_max": 280.91110000212706, + "noisy_sigma_init": 0.24583814763225417, + "minimum_profit_factor": 1.5028035348681215, + "weight_decay": 0.00010597074820791314, + "kelly_fractional": 0.5096243658522589, + "kelly_max_fraction": 0.12010947414322902, + "kelly_min_trades": 20, + "volatility_window": 23, + "use_ensemble_uncertainty": false, + "ensemble_size": 5.0, + "beta_variance": 0.5, + "beta_disagreement": 0.5, + "beta_entropy": 0.2, + "warmup_ratio": 0.0, + "curiosity_weight": 0.0, + "td_error_clamp_max": 10.0, + "batch_diversity_cooldown": 50.0, + "lr_decay_type": 0.0, + "sharpe_weight": 0.10176748614555509, + "gae_lambda": 0.95, + "noisy_sigma_initial": 0.5, + "noisy_sigma_final": 0.3, + "use_spectral_norm": true, + "use_attention": true, + "use_residual": true, + "norm_type": 1.0, + "activation_type": 1.0, + "num_quantiles": 32, + "qr_kappa": 1.0, + "iqn_lambda": 0.8479734309322158, + "spectral_norm_sigma_max": 4.8543579475657985, + "hidden_dim_base": 128, + "noisy_epsilon_floor": 0.1, + "count_bonus_coefficient": 0.23831546372779777, + "cql_alpha": 0.5059776371295834, + "eval_softmax_temp": 1.0, + "dsr_eta": 0.003947815597664894, + "branch_hidden_dim": 64, + "gradient_accumulation_steps": 1, + "w_dsr": 1.0, + "w_pnl": 0.3, + "w_dd": 1.0, + "w_idle": 0.01, + "dd_threshold": 0.01, + "loss_aversion": 1.5, + "time_decay_rate": 0.0005, + "q_gap_threshold": 0.1, + "c51_warmup_epochs": 10, + "dt_pretrain_epochs": 0, + "her_ratio": 0.48591734800375336, + "curiosity_weight_tunable": 0.09052455736721342, + "use_cvar_action_selection": 0.028233223186665324, + "cvar_alpha": 0.03929750758139713, + "min_hold_bars": 9 +} \ No newline at end of file diff --git a/crates/ml/ml/hyperopt_results/dqn/20260326_072108/campaign_summary.json b/crates/ml/ml/hyperopt_results/dqn/20260326_072108/campaign_summary.json new file mode 100644 index 000000000..7a336ab12 --- /dev/null +++ b/crates/ml/ml/hyperopt_results/dqn/20260326_072108/campaign_summary.json @@ -0,0 +1,7 @@ +{ + "trials_completed": 1, + "best_loss": 1000000.0, + "best_params_json": "{\n \"learning_rate\": 0.00005994999354431849,\n \"batch_size\": 307,\n \"gamma\": 0.9572818589229506,\n \"buffer_size\": 66246,\n \"max_position_absolute\": 1.1030284538648683,\n \"huber_delta\": 17.775790174607682,\n \"entropy_coefficient\": 0.38184099247597697,\n \"transaction_cost_multiplier\": 1.7738774066741243,\n \"per_alpha\": 0.45251155566697393,\n \"per_beta_start\": 0.20130083854118397,\n \"dueling_hidden_dim\": 128,\n \"n_steps\": 4,\n \"tau\": 0.008986246204051671,\n \"num_atoms\": 101,\n \"v_min\": -280.91110000212706,\n \"v_max\": 280.91110000212706,\n \"noisy_sigma_init\": 0.24583814763225417,\n \"minimum_profit_factor\": 1.5028035348681215,\n \"weight_decay\": 0.00010597074820791314,\n \"kelly_fractional\": 0.5096243658522589,\n \"kelly_max_fraction\": 0.12010947414322902,\n \"kelly_min_trades\": 20,\n \"volatility_window\": 23,\n \"use_ensemble_uncertainty\": false,\n \"ensemble_size\": 5.0,\n \"beta_variance\": 0.5,\n \"beta_disagreement\": 0.5,\n \"beta_entropy\": 0.2,\n \"warmup_ratio\": 0.0,\n \"curiosity_weight\": 0.0,\n \"td_error_clamp_max\": 10.0,\n \"batch_diversity_cooldown\": 50.0,\n \"lr_decay_type\": 0.0,\n \"sharpe_weight\": 0.10176748614555509,\n \"gae_lambda\": 0.95,\n \"noisy_sigma_initial\": 0.5,\n \"noisy_sigma_final\": 0.3,\n \"use_spectral_norm\": true,\n \"use_attention\": true,\n \"use_residual\": true,\n \"norm_type\": 1.0,\n \"activation_type\": 1.0,\n \"num_quantiles\": 32,\n \"qr_kappa\": 1.0,\n \"iqn_lambda\": 0.8479734309322158,\n \"spectral_norm_sigma_max\": 4.8543579475657985,\n \"hidden_dim_base\": 128,\n \"noisy_epsilon_floor\": 0.1,\n \"count_bonus_coefficient\": 0.23831546372779777,\n \"cql_alpha\": 0.5059776371295834,\n \"eval_softmax_temp\": 1.0,\n \"dsr_eta\": 0.003947815597664894,\n \"branch_hidden_dim\": 64,\n \"gradient_accumulation_steps\": 1,\n \"w_dsr\": 1.0,\n \"w_pnl\": 0.3,\n \"w_dd\": 1.0,\n \"w_idle\": 0.01,\n \"dd_threshold\": 0.01,\n \"loss_aversion\": 1.5,\n \"time_decay_rate\": 0.0005,\n \"q_gap_threshold\": 0.1,\n \"c51_warmup_epochs\": 10,\n \"dt_pretrain_epochs\": 0,\n \"her_ratio\": 0.48591734800375336,\n \"curiosity_weight_tunable\": 0.09052455736721342,\n \"use_cvar_action_selection\": 0.028233223186665324,\n \"cvar_alpha\": 0.03929750758139713,\n \"min_hold_bars\": 9\n}", + "best_checkpoint_path": null, + "total_time_seconds": 4.693175485 +} \ No newline at end of file diff --git a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs index ac28cb9b1..1bcf15077 100644 --- a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs +++ b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs @@ -3779,12 +3779,31 @@ fn compile_per_update_kernel(stream: &Arc) -> Result, - _config: &GpuDqnTrainConfig, + config: &GpuDqnTrainConfig, ) -> Result { + // c51_loss uses NUM_ATOMS for BOTH loop bounds AND array sizing — + // MUST be runtime-compiled with the correct #define injected. + let defines = format!( + "#define NUM_ATOMS {na}\n\ + #define V_MIN ({v_min:.1}f)\n\ + #define V_MAX ({v_max:.1}f)\n\ + #define BRANCH_0_SIZE {b0}\n\ + #define BRANCH_1_SIZE {b1}\n\ + #define BRANCH_2_SIZE {b2}\n", + na = config.num_atoms, + v_min = config.v_min, + v_max = config.v_max, + b0 = config.branch_0_size, + b1 = config.branch_1_size, + b2 = config.branch_2_size, + ); + let kernel_src = include_str!("c51_loss_kernel.cu"); + let full_source = format!("{defines}\n{kernel_src}"); let context = stream.context(); - let ptx = Ptx::from_binary(C51_LOSS_CUBIN.to_vec()); + let ptx = crate::cuda_pipeline::compile_ptx_for_device(&full_source, &context) + .map_err(|e| MLError::ModelError(format!("c51_loss_kernel compilation: {e}")))?; let module = context.load_module(ptx) - .map_err(|e| MLError::ModelError(format!("c51_loss cubin load: {e}")))?; + .map_err(|e| MLError::ModelError(format!("c51_loss module load: {e}")))?; module.load_function("c51_loss_batched") .map_err(|e| MLError::ModelError(format!("c51_loss_batched load: {e}"))) } @@ -3804,13 +3823,31 @@ fn compile_c51_grad_kernel( .map_err(|e| MLError::ModelError(format!("c51_grad_kernel load: {e}"))) } -/// Load the MSE loss kernel from precompiled cubin (ZERO runtime nvcc). +/// Compile MSE loss kernel at runtime with correct NUM_ATOMS injected. +/// MSE uses NUM_ATOMS for loop bounds — MUST match runtime config. fn compile_mse_loss_kernel( stream: &Arc, - _config: &GpuDqnTrainConfig, + config: &GpuDqnTrainConfig, ) -> Result { + let defines = format!( + "#define NUM_ATOMS {na}\n\ + #define V_MIN ({v_min:.1}f)\n\ + #define V_MAX ({v_max:.1}f)\n\ + #define BRANCH_0_SIZE {b0}\n\ + #define BRANCH_1_SIZE {b1}\n\ + #define BRANCH_2_SIZE {b2}\n", + na = config.num_atoms, + v_min = config.v_min, + v_max = config.v_max, + b0 = config.branch_0_size, + b1 = config.branch_1_size, + b2 = config.branch_2_size, + ); + let kernel_src = include_str!("mse_loss_kernel.cu"); + let full_source = format!("{defines}\n{kernel_src}"); let context = stream.context(); - let ptx = Ptx::from_binary(MSE_LOSS_CUBIN.to_vec()); + let ptx = crate::cuda_pipeline::compile_ptx_for_device(&full_source, &context) + .map_err(|e| MLError::ModelError(format!("mse_loss_kernel compilation: {e}")))?; let module = context.load_module(ptx) .map_err(|e| MLError::ModelError(format!("mse_loss cubin load: {e}")))?; module.load_function("mse_loss_batched")