cleanup(ml): delete crates/ml/src/trainers/tlob.rs — orphan vaporware trainer

The TLOBTrainer in this file was:
- Never referenced by any caller in crates/ /services/ /bin/
  (grep confirms zero hits beyond the string literal "TLOB" in
  ml-ensemble's adaptive weight maps, which does not depend on the
  trainer type).
- save_checkpoint and serialize_model logged "saving" but wrote
  zero bytes, then immediately called std::fs::metadata.len() and
  std::fs::read() on the missing file → ENOENT every time. Ghost
  feature masquerading as a trainer (flagged during the pre-L40S
  bulk-TODO sweep, commit 155c079fa).
- Depended on an ONNX-Runtime path in crates/ml-supervised/src/tlob/
  transformer.rs (Environment / Session / Value), incompatible with
  Foxhunt's native cudarc + cuBLAS GPU path used everywhere else.

TLOB feature-extractors in crates/ml-supervised/src/tlob/{features,
mbp10_feature_extractor,analytics,performance}.rs ARE retained —
they produce a 51-dim order-book feature representation that is
potentially worth integrating into the DQN data pipeline as a richer
alternative to our current 20-dim OFI. That integration is tracked
as a separate follow-up, NOT attempted here.

Also removes:
- `pub mod tlob;` in crates/ml/src/trainers/mod.rs:80
- `pub use tlob::{TLOBHyperparameters, TLOBTrainer, TLOBTrainingMetrics};`
  re-export in trainers/mod.rs:109

Full workspace check: cargo check --workspace passes, no new warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-23 09:06:31 +02:00
parent 155c079fa5
commit ac959f807f
2 changed files with 0 additions and 689 deletions

View File

@@ -77,7 +77,6 @@ pub mod online_learning;
pub mod ppo;
pub mod tft;
pub mod tft_parquet; // DBN/OHLCV bar training extension for TFT
pub mod tlob;
/// Target network update strategy for DQN training
#[derive(Debug, Clone, Copy)]
@@ -107,4 +106,3 @@ pub use tft::{
TrainingMetrics as TFTTrainingMetrics, TrainingProgress as TFTTrainingProgress,
};
pub use liquid::{LiquidHyperparameters, LiquidTrainer, LiquidTrainingMetrics};
pub use tlob::{TLOBHyperparameters, TLOBTrainer, TLOBTrainingMetrics};

View File

@@ -1,687 +0,0 @@
//! TLOB Transformer Trainer with gRPC Integration
//!
//! Production-ready TLOB trainer optimized for Level-2 order book data with GPU acceleration.
//! Designed to train transformer models for price movement prediction using MBP-10 (Market By Price) data.
//!
//! ## Features
//!
//! - GPU acceleration (RTX 3050 Ti compatible)
//! - Level-2 order book sequence training (10 price levels)
//! - Checkpoint management with MinIO/S3 integration
//! - Real-time training progress streaming
//! - MSE loss for price movement prediction
//! - 51-feature extraction from order book snapshots
//!
//! ## Architecture
//!
//! - Transformer encoder with multi-head attention
//! - Sequence length: 128 order book snapshots
//! - Input features: 51 microstructure features per snapshot
//! - Output: Next price movement prediction
//! - Training objective: MSE loss on price changes
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;
use anyhow::{Context, Result};
use ml_core::device::MlDevice;
use ml_core::cuda_autograd::GpuTensor;
use ml_core::cuda_autograd::GpuVarStore;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::{info, instrument, warn};
use crate::tlob::features::TLOB_FEATURE_COUNT;
use crate::tlob::transformer::TLOBTransformer;
/// TLOB training hyperparameters from gRPC request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TLOBHyperparameters {
/// Learning rate (typically 1e-4 to 1e-5)
pub learning_rate: f64,
/// Batch size (must be ≤32 for 4GB VRAM)
pub batch_size: usize,
/// Sequence length (number of order book snapshots)
pub seq_len: usize,
/// Number of price levels (10 for MBP-10)
pub num_price_levels: usize,
/// Transformer hidden dimension
pub d_model: usize,
/// Number of attention heads
pub num_heads: usize,
/// Number of transformer layers
pub num_layers: usize,
/// Dropout rate
pub dropout: f64,
/// Number of training epochs
pub epochs: usize,
/// Checkpoint save frequency (epochs)
pub checkpoint_frequency: usize,
/// Gradient clipping threshold
pub grad_clip: f64,
/// Weight decay for regularization
pub weight_decay: f64,
}
impl Default for TLOBHyperparameters {
fn default() -> Self {
Self {
learning_rate: 0.0001,
batch_size: 16, // Conservative for 4GB VRAM
seq_len: 128, // Order book snapshot sequence
num_price_levels: 10, // MBP-10
d_model: 256, // Transformer hidden size
num_heads: 8, // Multi-head attention
num_layers: 4, // Transformer blocks
dropout: 0.1,
epochs: 500, // TLOB needs more epochs
checkpoint_frequency: 10,
grad_clip: 1.0,
weight_decay: 0.0001,
}
}
}
/// Training progress metrics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TLOBTrainingMetrics {
pub epoch: usize,
pub train_loss: f64,
pub val_loss: f64,
pub avg_mae: f64, // Mean Absolute Error
pub avg_prediction_error: f64,
pub gradient_norm: f64,
pub learning_rate: f64,
pub elapsed_seconds: f64,
}
/// TLOB Trainer with gRPC integration
pub struct TLOBTrainer {
/// Model configuration
hyperparams: TLOBHyperparameters,
/// TLOB transformer model (used for ONNX-based inference / fallback)
model: Arc<RwLock<TLOBTransformer>>,
/// GPU input projection layer for gradient-based training.
/// Maps flattened order book features (seq_len * feature_dim) -> d_model.
input_projection: ml_core::cuda_autograd::GpuLinear,
/// GPU output projection layer for gradient-based training.
/// Maps d_model -> 1 (scalar prediction).
output_projection: ml_core::cuda_autograd::GpuLinear,
/// GPU variable store for model parameters (replaces VarMap + AdamW)
var_store: GpuVarStore,
/// Device (GPU)
device: MlDevice,
/// Checkpoint directory
checkpoint_dir: PathBuf,
/// Best validation loss
best_val_loss: f64,
/// Training start time
start_time: Option<Instant>,
}
impl std::fmt::Debug for TLOBTrainer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TLOBTrainer")
.field("hyperparams", &self.hyperparams)
.field("device", &self.device)
.field("checkpoint_dir", &self.checkpoint_dir)
.field("best_val_loss", &self.best_val_loss)
.finish_non_exhaustive()
}
}
impl TLOBTrainer {
/// Create new TLOB trainer with hyperparameters
///
/// # Arguments
///
/// * `hyperparams` - Training hyperparameters from gRPC request
/// * `checkpoint_dir` - Directory for saving model checkpoints
/// * `use_gpu` - Whether to use GPU acceleration (RTX 3050 Ti)
pub fn new<P: AsRef<Path>>(
hyperparams: TLOBHyperparameters,
checkpoint_dir: P,
use_gpu: bool,
) -> Result<Self> {
// Validate batch size for GPU memory
const MAX_BATCH_SIZE: usize = 32;
if use_gpu && hyperparams.batch_size > MAX_BATCH_SIZE {
warn!(
"Batch size {} exceeds GPU limit ({}), using CPU instead",
hyperparams.batch_size, MAX_BATCH_SIZE
);
}
// Create device — CUDA is mandatory, CPU execution FORBIDDEN
let device = if use_gpu && hyperparams.batch_size <= MAX_BATCH_SIZE {
MlDevice::cuda(0).map_err(|e| {
anyhow::anyhow!("CUDA device required but unavailable: {}", e)
})?
} else if !use_gpu {
return Err(anyhow::anyhow!(
"TLOB trainer requires CUDA — CPU execution FORBIDDEN"
));
} else {
// batch_size > MAX_BATCH_SIZE with GPU requested
return Err(anyhow::anyhow!(
"TLOB batch size {} exceeds GPU limit {} — reduce batch size",
hyperparams.batch_size,
MAX_BATCH_SIZE
));
};
info!("Using GPU device: {:?}", device);
info!(
"Initializing TLOB trainer: seq_len={}, d_model={}, num_layers={}, device={:?}",
hyperparams.seq_len, hyperparams.d_model, hyperparams.num_layers, device
);
// Create checkpoint directory
let checkpoint_path = checkpoint_dir.as_ref().to_path_buf();
std::fs::create_dir_all(&checkpoint_path)
.context("Failed to create checkpoint directory")?;
// Initialize GPU variable store
let stream = device.cuda_stream().map_err(|e| {
anyhow::anyhow!("TLOB requires CUDA stream: {e}")
})?;
let mut var_store = GpuVarStore::new(Arc::clone(stream));
// Create TLOB transformer model (ONNX-based inference / fallback)
let model = Self::create_trainable_model(&hyperparams, &device)?;
// Create GPU projection layers registered in the var store
// Architecture: flatten(seq_len * feature_dim) -> d_model -> ReLU -> 1
let input_dim = hyperparams.seq_len * TLOB_FEATURE_COUNT;
let input_projection = var_store.linear(
"input_proj",
input_dim,
hyperparams.d_model,
)?;
let output_projection = var_store.linear(
"output_proj",
hyperparams.d_model,
1,
)?;
Ok(Self {
hyperparams,
model: Arc::new(RwLock::new(model)),
input_projection,
output_projection,
var_store,
device,
checkpoint_dir: checkpoint_path,
best_val_loss: f64::INFINITY,
start_time: None,
})
}
/// Create trainable TLOB transformer model
///
/// NOTE: This is a placeholder implementation. The actual TLOBTransformer
/// needs to be updated with a trainable constructor that accepts VarBuilder.
fn create_trainable_model(
hyperparams: &TLOBHyperparameters,
_device: &MlDevice,
) -> Result<TLOBTransformer> {
// Placeholder: Create TLOBTransformer with default config
// This will be replaced when TLOBTransformer gets a trainable constructor
use crate::tlob::transformer::TLOBConfig;
let config = TLOBConfig {
model_path: "".to_owned(), // Not used for training
feature_dim: TLOB_FEATURE_COUNT,
prediction_horizon: 10,
batch_size: hyperparams.batch_size,
device: "cuda".to_owned(),
};
TLOBTransformer::new(config)
.map_err(|e| anyhow::anyhow!("Failed to create TLOB transformer: {:?}", e))
}
/// Forward pass through the Candle projection layers.
///
/// Flattens `(batch, seq_len, feature_dim)` input to `(batch, seq_len*feature_dim)`,
/// then applies `input_projection -> ReLU -> output_projection` to produce `(batch, 1)`.
fn forward_projection(&self, input: &GpuTensor, stream: &Arc<cudarc::driver::CudaStream>) -> Result<GpuTensor> {
let cublas = cudarc::cublas::CudaBlas::new(Arc::clone(stream))
.map_err(|e| anyhow::anyhow!("cuBLAS init: {e}"))?;
// Forward through input projection: [batch, input_dim] -> [batch, d_model]
let (hidden, _acts) = self.input_projection.forward(
input, &self.var_store, &cublas, stream,
)?;
// ReLU activation on host (small tensor, not hot path for TLOB)
let mut hidden_host = hidden.to_host(stream)?;
for v in &mut hidden_host {
if *v < 0.0 { *v = 0.0; }
}
let activated = GpuTensor::from_host(&hidden_host, hidden.shape().to_vec(), stream)?; // cpu-side shape clone
// Output projection: [batch, d_model] -> [batch, 1]
let (output, _acts) = self.output_projection.forward(
&activated, &self.var_store, &cublas, stream,
)?;
Ok(output)
}
/// Train TLOB model on Level-2 order book data
///
/// # Arguments
///
/// * `data_dir` - Directory containing L2 order book data (from Agent 71)
/// * `progress_callback` - Callback for reporting progress to gRPC stream
///
/// # Returns
///
/// Final training metrics
#[instrument(skip(self, progress_callback))]
pub async fn train<F>(
&mut self,
data_dir: &str,
mut progress_callback: F,
) -> Result<TLOBTrainingMetrics>
where
F: FnMut(TLOBTrainingMetrics) + Send,
{
info!(
"Starting TLOB training for {} epochs with batch size {}",
self.hyperparams.epochs, self.hyperparams.batch_size
);
self.start_time = Some(Instant::now());
// Load order book data (placeholder - requires Agent 71 implementation)
let (train_sequences, val_sequences) = self
.load_order_book_data(data_dir)
.await
.context("Failed to load order book data")?;
info!(
"Loaded {} training sequences, {} validation sequences",
train_sequences.len(),
val_sequences.len()
);
let mut final_metrics = TLOBTrainingMetrics {
epoch: 0,
train_loss: 0.0,
val_loss: 0.0,
avg_mae: 0.0,
avg_prediction_error: 0.0,
gradient_norm: 0.0,
learning_rate: self.hyperparams.learning_rate,
elapsed_seconds: 0.0,
};
// Main training loop
for epoch in 0..self.hyperparams.epochs {
let epoch_start = Instant::now();
// Training phase
let train_loss = self.train_epoch(&train_sequences).await?;
// Validation phase
let (val_loss, mae) = self.validate_epoch(&val_sequences).await?;
// Calculate metrics
let elapsed = self.start_time
.unwrap_or_else(|| Instant::now())
.elapsed()
.as_secs_f64();
let grad_norm = self.calculate_gradient_norm()?;
let metrics = TLOBTrainingMetrics {
epoch: epoch + 1,
train_loss,
val_loss,
avg_mae: mae,
avg_prediction_error: mae, // Same as MAE for regression
gradient_norm: grad_norm,
learning_rate: self.hyperparams.learning_rate,
elapsed_seconds: elapsed,
};
// Report progress
progress_callback(metrics.clone());
final_metrics = metrics.clone();
info!(
"Epoch {}/{}: train_loss={:.6}, val_loss={:.6}, mae={:.6}, grad_norm={:.6}, time={:.2}s",
epoch + 1,
self.hyperparams.epochs,
train_loss,
val_loss,
mae,
grad_norm,
epoch_start.elapsed().as_secs_f64()
);
// Save checkpoint if best validation loss
if val_loss < self.best_val_loss {
self.best_val_loss = val_loss;
info!("New best validation loss: {:.6}", val_loss);
}
// Save checkpoint every N epochs
if (epoch + 1) % self.hyperparams.checkpoint_frequency == 0 {
self.save_checkpoint(epoch + 1).await?;
}
}
info!(
"Training completed in {:.2}s: final_val_loss={:.6}, best_val_loss={:.6}",
final_metrics.elapsed_seconds, final_metrics.val_loss, self.best_val_loss
);
Ok(final_metrics)
}
/// Train one epoch
///
/// Loss is accumulated on-device as a GPU tensor to avoid per-batch
/// `to_scalar()` synchronisation. A single scalar extraction happens
/// after the loop completes, with a NaN guard every 100 batches.
async fn train_epoch(&mut self, sequences: &[OrderBookSequence]) -> Result<f64> {
let stream = self.device.cuda_stream().map_err(|_e| {
anyhow::anyhow!("TLOB train_epoch requires CUDA stream")
})?;
let stream = Arc::clone(stream);
let mut total_loss = 0.0_f64;
let mut num_batches: usize = 0;
// Process in batches
for batch_sequences in sequences.chunks(self.hyperparams.batch_size) {
let (input_tensor, target_tensor) = self.prepare_batch(batch_sequences, &stream)?;
// Forward pass
let predictions = self.forward_projection(&input_tensor, &stream)?;
// Calculate MSE loss on host (small batch)
let pred_host = predictions.to_host(&stream)?;
let target_host = target_tensor.to_host(&stream)?;
let batch_loss: f64 = pred_host.iter().zip(target_host.iter())
.map(|(p, t)| { let d = (*p - *t) as f64; d * d })
.sum::<f64>() / pred_host.len().max(1) as f64;
total_loss += batch_loss;
num_batches += 1;
// NaN guard every 100 batches
if num_batches % 100 == 0 && (total_loss.is_nan() || total_loss.is_infinite()) {
warn!(batch = num_batches, "TLOB train loss diverged (NaN/Inf)");
return Ok(f64::NAN);
}
}
if num_batches == 0 {
return Ok(0.0);
}
Ok(total_loss / num_batches as f64)
}
/// Validate one epoch
///
/// Loss is accumulated on-device to avoid per-batch `to_scalar()`.
/// MAE already returns `f64` (CPU scalar) so it stays as f64 sum.
async fn validate_epoch(&self, sequences: &[OrderBookSequence]) -> Result<(f64, f64)> {
let stream = self.device.cuda_stream().map_err(|_e| {
anyhow::anyhow!("TLOB validate_epoch requires CUDA stream")
})?;
let stream = Arc::clone(stream);
let mut total_loss = 0.0_f64;
let mut total_mae = 0.0_f64;
let mut num_batches: usize = 0;
for batch_sequences in sequences.chunks(self.hyperparams.batch_size) {
let (input_tensor, target_tensor) = self.prepare_batch(batch_sequences, &stream)?;
let predictions = self.forward_projection(&input_tensor, &stream)?;
let pred_host = predictions.to_host(&stream)?;
let target_host = target_tensor.to_host(&stream)?;
let batch_loss: f64 = pred_host.iter().zip(target_host.iter())
.map(|(p, t)| { let d = (*p - *t) as f64; d * d })
.sum::<f64>() / pred_host.len().max(1) as f64;
let batch_mae: f64 = pred_host.iter().zip(target_host.iter())
.map(|(p, t)| (*p - *t).abs() as f64)
.sum::<f64>() / pred_host.len().max(1) as f64;
total_loss += batch_loss;
total_mae += batch_mae;
num_batches += 1;
}
if num_batches == 0 {
return Ok((0.0, 0.0));
}
Ok((total_loss / num_batches as f64, total_mae / num_batches as f64))
}
/// Prepare batch tensors from order book sequences
fn prepare_batch(&self, sequences: &[OrderBookSequence], stream: &Arc<cudarc::driver::CudaStream>) -> Result<(GpuTensor, GpuTensor)> {
let batch_size = sequences.len();
let seq_len = self.hyperparams.seq_len;
let feature_dim = TLOB_FEATURE_COUNT;
let flat_dim = seq_len * feature_dim;
let mut input_data = Vec::with_capacity(batch_size * flat_dim);
let mut target_data = Vec::with_capacity(batch_size);
for seq in sequences {
for snapshot in &seq.snapshots {
input_data.extend_from_slice(&snapshot.features);
}
target_data.push(seq.target_price_change);
}
let input_tensor = GpuTensor::from_host(&input_data, vec![batch_size, seq_len, feature_dim], stream)?;
let target_tensor = GpuTensor::from_host(&target_data, vec![batch_size, 1], stream)?;
Ok((input_tensor, target_tensor))
}
/// Calculate gradient norm by downloading parameters (monitoring only).
fn calculate_gradient_norm(&self) -> Result<f64> {
// Parameter norm (not gradient norm) — monitors for weight explosion
let stream = self.device.cuda_stream().map_err(|_e| {
anyhow::anyhow!("TLOB requires CUDA stream")
})?;
let mut total_sq = 0.0_f64;
for (name, param) in self.var_store.iter() {
let mut host = vec![0.0_f32; param.data.len()];
crate::cuda_pipeline::dtoh_f32(&stream, &param.data, &mut host)
.map_err(|e| anyhow::anyhow!("param {} DtoH: {e}", name))?;
let sq: f64 = host.iter().map(|&v| (v as f64) * (v as f64)).sum();
total_sq += sq;
}
Ok(total_sq.sqrt())
}
/// Save model checkpoint
#[instrument(skip(self))]
async fn save_checkpoint(&self, epoch: usize) -> Result<()> {
let checkpoint_path = self
.checkpoint_dir
.join(format!("tlob_epoch_{}.safetensors", epoch));
info!("Saving checkpoint to: {}", checkpoint_path.display());
// GpuVarStore does not yet expose safetensors serialisation, so
// no parameters are written to `checkpoint_path`. The fs::metadata
// call below therefore fails — `save_checkpoint` is currently a
// non-functional hook kept so the training loop has a stable
// call site for when serialisation lands.
let _ = &checkpoint_path;
info!(
"Checkpoint saved: {} bytes",
std::fs::metadata(&checkpoint_path)?.len()
);
Ok(())
}
/// Load order book data from directory
///
/// NOTE: This is a placeholder. Actual implementation depends on Agent 71's
/// TLOBDataLoader for loading Level-2 order book data.
async fn load_order_book_data(
&self,
_data_dir: &str,
) -> Result<(Vec<OrderBookSequence>, Vec<OrderBookSequence>)> {
// Placeholder: Generate dummy data for compilation
warn!("Using dummy order book data - Agent 71 L2 data loader not yet implemented");
let train_sequences = self.generate_dummy_sequences(100)?;
let val_sequences = self.generate_dummy_sequences(20)?;
Ok((train_sequences, val_sequences))
}
/// Generate dummy order book sequences for testing
fn generate_dummy_sequences(&self, count: usize) -> Result<Vec<OrderBookSequence>> {
use rand::Rng;
let mut rng = rand::thread_rng();
let mut sequences = Vec::with_capacity(count);
for _ in 0..count {
let mut snapshots = Vec::with_capacity(self.hyperparams.seq_len);
for _ in 0..self.hyperparams.seq_len {
let features: Vec<f32> = (0..TLOB_FEATURE_COUNT)
.map(|_| rng.gen_range(-1.0..1.0))
.collect();
snapshots.push(OrderBookSnapshot { features });
}
let target_price_change = rng.gen_range(-0.01..0.01);
sequences.push(OrderBookSequence {
snapshots,
target_price_change,
});
}
Ok(sequences)
}
/// Serialize model to bytes
pub async fn serialize_model(&self) -> Result<Vec<u8>> {
let temp_path =
std::env::temp_dir().join(format!("tlob_{}.safetensors", uuid::Uuid::new_v4()));
// GpuVarStore serialisation is not wired; this function keeps
// the signature for callers but the subsequent `fs::read` will
// error out because nothing writes `temp_path`.
let _ = &temp_path;
let data = std::fs::read(&temp_path).context("Failed to read checkpoint")?;
drop(std::fs::remove_file(&temp_path));
Ok(data)
}
}
/// Order book sequence (128 snapshots + target)
#[derive(Debug, Clone)]
struct OrderBookSequence {
snapshots: Vec<OrderBookSnapshot>,
target_price_change: f32,
}
/// Single order book snapshot (51 features)
#[derive(Debug, Clone)]
struct OrderBookSnapshot {
features: Vec<f32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_tlob_trainer_creation() {
let hyperparams = TLOBHyperparameters::default();
let temp_dir = std::env::temp_dir().join("tlob_test");
let trainer = TLOBTrainer::new(hyperparams, &temp_dir, true);
assert!(
trainer.is_ok(),
"Failed to create TLOB trainer: {:?}",
trainer.err()
);
}
#[tokio::test]
async fn test_batch_size_validation() {
let mut hyperparams = TLOBHyperparameters::default();
hyperparams.batch_size = 64;
let temp_dir = std::env::temp_dir().join("tlob_test_batch");
let trainer = TLOBTrainer::new(hyperparams, &temp_dir, true);
assert!(
trainer.is_err(),
"Batch size 64 exceeds GPU limit 32 — should be rejected"
);
}
#[tokio::test]
async fn test_dummy_sequence_generation() {
let hyperparams = TLOBHyperparameters::default();
let temp_dir = std::env::temp_dir().join("tlob_test_seq");
let trainer = TLOBTrainer::new(hyperparams, &temp_dir, true).unwrap();
let sequences = trainer.generate_dummy_sequences(10).unwrap();
assert_eq!(sequences.len(), 10);
assert_eq!(sequences[0].snapshots.len(), 128);
assert_eq!(sequences[0].snapshots[0].features.len(), TLOB_FEATURE_COUNT);
}
#[tokio::test]
async fn test_batch_preparation() {
let hyperparams = TLOBHyperparameters::default();
let temp_dir = std::env::temp_dir().join("tlob_test_batch_prep");
let trainer = TLOBTrainer::new(hyperparams, &temp_dir, true).unwrap();
let sequences = trainer.generate_dummy_sequences(4).unwrap();
let dev = MlDevice::cuda(0).expect("CUDA required");
let stream = dev.cuda_stream().expect("stream");
let (input_tensor, target_tensor) = trainer.prepare_batch(&sequences, stream).unwrap();
assert_eq!(input_tensor.dims(), &[4, 128, TLOB_FEATURE_COUNT]);
assert_eq!(target_tensor.dims(), &[4, 1]);
}
}