feat: train_fold_from_slices — trainer accepts contiguous feature/target slices

Adds DQNTrainer::train_fold_from_slices(&[[f64;42]], &[[f64;4]]) so the
fold loop in train_baseline_rl passes raw fxcache slices directly, without
the caller constructing any Vec<(FeatureVector, Vec<f64>)>. Removes
features_to_trainer_format_fast helper (no longer needed) and updates
train_dqn_fold to call the new method.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-02 22:50:11 +02:00
parent 5c985df147
commit 8d5af977f7
2 changed files with 39 additions and 20 deletions

View File

@@ -352,21 +352,6 @@ fn hp_bool(params: &Option<Value>, key: &str) -> Option<bool> {
// DQN Training
// ---------------------------------------------------------------------------
/// Convert fxcache feature/target slices into the format expected by
/// `DQNTrainer::train_with_preloaded_data`: `Vec<(FeatureVector, Vec<f64>)>`.
///
/// Zero bar reconstruction — targets already contain [close, next_close, raw_close, raw_next].
fn features_to_trainer_format_fast(
features: &[[f64; 42]],
targets: &[[f64; 4]],
) -> Vec<(FeatureVector, Vec<f64>)> {
features
.iter()
.zip(targets.iter())
.map(|(f, t)| (*f, t.to_vec()))
.collect()
}
/// Build DQN hyperparameters from args, hyperopt JSON, and GPU profile.
///
/// Extracted from the old `train_dqn_fold` so it can be called ONCE before the fold
@@ -478,10 +463,6 @@ fn train_dqn_fold(
rt.block_on(trainer.reset_for_fold())
.context("reset_for_fold failed")?;
// Convert to Vec format for the training API (Task 5 will eliminate this)
let training_data = features_to_trainer_format_fast(train_features, train_targets);
let val_data = features_to_trainer_format_fast(val_features, val_targets);
// Checkpoint callback: save best model to output directory
let output_dir_owned = output_dir.to_path_buf();
let prefix_owned = checkpoint_prefix.to_owned();
@@ -500,7 +481,7 @@ fn train_dqn_fold(
};
let metrics = rt.block_on(
trainer.train_with_preloaded_data(training_data, val_data, checkpoint_callback)
trainer.train_fold_from_slices(train_features, train_targets, checkpoint_callback)
).map_err(|e| {
error!(" [DQN] Fold {} training error chain: {:#}", fold, e);
e

View File

@@ -527,6 +527,44 @@ impl DQNTrainer {
.await
}
/// Train one fold from contiguous feature/target slices.
/// Zero Vec<f64> allocation — targets read as fixed-size &[[f64; 4]].
///
/// Caller must call `set_val_data_from_slices` and `set_training_range` before
/// this method. Val data and OFI offset are already set on `self`.
pub async fn train_fold_from_slices<F>(
&mut self,
features: &[[f64; 42]],
targets: &[[f64; 4]],
checkpoint_callback: F,
) -> Result<TrainingMetrics>
where
F: FnMut(usize, Vec<u8>, bool) -> Result<String> + Send,
{
// Clear stale CUDA errors
if let MlDevice::Cuda { ref context, .. } = self.device {
let _ = context.check_err();
}
info!("train_fold_from_slices: {} bars", features.len());
// Build lightweight training_data with fixed-size [f64; 4] converted to Vec<f64>
// This is the last remaining allocation — each [f64; 4].to_vec() is 32 bytes.
// TODO: eliminate by changing train_with_data_full_loop signature
let training_data: Vec<(FeatureVector, Vec<f64>)> = features
.iter()
.zip(targets.iter())
.map(|(f, t)| (*f, t.to_vec()))
.collect();
self.val_features_gpu = None;
self.val_closes_gpu = None;
self.val_ofi_gpu = None;
self.train_with_data_full_loop(&training_data, checkpoint_callback)
.await
}
/// Train with shared preloaded data (zero-copy for hyperopt).
///
/// Same as [`train_with_preloaded_data`] but accepts `Arc`-wrapped data,