refactor: mod.rs bf16 helpers → direct f32 transfers, legacy aliases for incremental migration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
//! pre-uploaded buffers instead of creating per-step copies.
|
||||
//!
|
||||
//! All modules operate on raw `CudaSlice` via cudarc.
|
||||
//! `DqnGpuData` and `PpoGpuData` hold GPU-resident `CudaSlice<half::bf16>` buffers.
|
||||
//! `DqnGpuData` and `PpoGpuData` hold GPU-resident `CudaSlice<f32>` buffers.
|
||||
|
||||
use std::sync::Arc;
|
||||
use cudarc::driver::{CudaSlice, CudaStream, DevicePtr, DevicePtrMut};
|
||||
@@ -52,72 +52,74 @@ pub fn align_to_tensor_cores(dim: usize) -> usize {
|
||||
|
||||
/// Estimate VRAM usage in bytes for pre-uploaded f32 data.
|
||||
pub fn estimate_vram_bytes(num_elements: usize) -> usize {
|
||||
num_elements * std::mem::size_of::<half::bf16>()
|
||||
num_elements * std::mem::size_of::<f32>()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BF16 ↔ F32 host-side conversion helpers
|
||||
// F32 host ↔ device transfer helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
// All GPU buffers are CudaSlice<half::bf16>. Host code works in f32.
|
||||
// These helpers bridge the boundary at every HtoD / DtoH transfer.
|
||||
// All GPU buffers are CudaSlice<f32>. Host code works in f32.
|
||||
// Direct transfers — no bf16 conversion.
|
||||
|
||||
/// Convert an f32 slice to a Vec of bf16 for GPU upload.
|
||||
#[inline]
|
||||
pub fn f32_slice_to_bf16(src: &[f32]) -> Vec<half::bf16> {
|
||||
src.iter().map(|&x| half::bf16::from_f32(x)).collect()
|
||||
}
|
||||
|
||||
/// Upload f32 host data into an existing `CudaSlice<half::bf16>`.
|
||||
///
|
||||
/// Converts f32 → bf16 on the host, then calls `memcpy_htod`.
|
||||
pub fn htod_f32_to_bf16(
|
||||
/// Upload f32 host data into an existing `CudaSlice<f32>`.
|
||||
pub fn htod_f32(
|
||||
stream: &Arc<CudaStream>,
|
||||
src: &[f32],
|
||||
dst: &mut CudaSlice<half::bf16>,
|
||||
dst: &mut CudaSlice<f32>,
|
||||
) -> Result<(), MLError> {
|
||||
let bf16_data = f32_slice_to_bf16(src);
|
||||
stream.memcpy_htod(&bf16_data, dst)
|
||||
.map_err(|e| MLError::ModelError(format!("htod_f32_to_bf16: {e}")))
|
||||
stream.memcpy_htod(src, dst)
|
||||
.map_err(|e| MLError::ModelError(format!("htod_f32: {e}")))
|
||||
}
|
||||
|
||||
/// Upload f32 host data to a new `CudaSlice<half::bf16>` (clone_htod equivalent).
|
||||
pub fn clone_htod_f32_to_bf16(
|
||||
/// Upload f32 host data to a new `CudaSlice<f32>` (clone_htod).
|
||||
pub fn clone_htod_f32(
|
||||
stream: &Arc<CudaStream>,
|
||||
src: &[f32],
|
||||
) -> Result<CudaSlice<half::bf16>, MLError> {
|
||||
let bf16_data = f32_slice_to_bf16(src);
|
||||
stream.clone_htod(&bf16_data)
|
||||
.map_err(|e| MLError::ModelError(format!("clone_htod_f32_to_bf16: {e}")))
|
||||
) -> Result<CudaSlice<f32>, MLError> {
|
||||
stream.clone_htod(src)
|
||||
.map_err(|e| MLError::ModelError(format!("clone_htod_f32: {e}")))
|
||||
}
|
||||
|
||||
/// Download `CudaSlice<half::bf16>` (or `CudaView<half::bf16>`) to an f32 host buffer.
|
||||
/// Download `CudaSlice<f32>` (or `CudaView<f32>`) to an f32 host buffer.
|
||||
///
|
||||
/// Allocates a temporary bf16 host buffer, downloads, then converts to f32.
|
||||
pub fn dtoh_bf16_to_f32<Src: cudarc::driver::DevicePtr<half::bf16>>(
|
||||
/// Uses raw cuStreamSynchronize + cuMemcpyDtoH_v2 for CUDA-graph safety
|
||||
/// (avoids cudarc event tracking which can deadlock inside captured graphs).
|
||||
pub fn dtoh_f32<Src: cudarc::driver::DevicePtr<f32>>(
|
||||
stream: &Arc<CudaStream>,
|
||||
src: &Src,
|
||||
dst: &mut [f32],
|
||||
) -> Result<(), MLError> {
|
||||
let n = dst.len();
|
||||
let mut bf16_buf = vec![half::bf16::ZERO; n];
|
||||
let num_bytes = n * std::mem::size_of::<half::bf16>();
|
||||
let num_bytes = n * std::mem::size_of::<f32>();
|
||||
let (src_ptr, _guard) = src.device_ptr(stream);
|
||||
// Sync + raw DtoH to avoid cudarc event tracking (graph-safe)
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
cudarc::driver::sys::cuStreamSynchronize(stream.cu_stream());
|
||||
cudarc::driver::sys::cuMemcpyDtoH_v2(
|
||||
bf16_buf.as_mut_ptr().cast(),
|
||||
dst.as_mut_ptr().cast(),
|
||||
src_ptr,
|
||||
num_bytes,
|
||||
);
|
||||
}
|
||||
for (d, &s) in dst.iter_mut().zip(bf16_buf.iter()) {
|
||||
*d = s.to_f32();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Legacy aliases — delegate to the f32 functions above.
|
||||
// Keeps the 188 existing call sites compiling during incremental migration.
|
||||
#[inline]
|
||||
pub fn htod_f32_to_bf16(stream: &Arc<CudaStream>, src: &[f32], dst: &mut CudaSlice<f32>) -> Result<(), MLError> {
|
||||
htod_f32(stream, src, dst)
|
||||
}
|
||||
#[inline]
|
||||
pub fn clone_htod_f32_to_bf16(stream: &Arc<CudaStream>, src: &[f32]) -> Result<CudaSlice<f32>, MLError> {
|
||||
clone_htod_f32(stream, src)
|
||||
}
|
||||
#[inline]
|
||||
pub fn dtoh_bf16_to_f32<Src: cudarc::driver::DevicePtr<f32>>(stream: &Arc<CudaStream>, src: &Src, dst: &mut [f32]) -> Result<(), MLError> {
|
||||
dtoh_f32(stream, src, dst)
|
||||
}
|
||||
|
||||
/// Compute optimal (grid_dim, block_dim) for a 1-D kernel launch.
|
||||
///
|
||||
/// Selects the largest block size that divides 32 (warp size) and does not
|
||||
@@ -167,21 +169,21 @@ pub fn optimal_launch_dims(n_items: u32, max_threads_per_block: u32) -> (u32, u3
|
||||
/// The fused experience collector kernel (4490 lines, branching+C51+NoisyNets)
|
||||
// compile_ptx_for_device re-export removed: all kernels now use precompiled cubins.
|
||||
|
||||
/// Clone a `CudaSlice<half::bf16>` via DtoD memcpy.
|
||||
/// Clone a `CudaSlice<f32>` via DtoD memcpy.
|
||||
///
|
||||
/// The caller owns the returned slice independently of the source.
|
||||
pub fn clone_cuda_slice_f32(
|
||||
src: &CudaSlice<half::bf16>,
|
||||
src: &CudaSlice<f32>,
|
||||
stream: &Arc<CudaStream>,
|
||||
) -> Result<CudaSlice<half::bf16>, crate::MLError> {
|
||||
) -> Result<CudaSlice<f32>, crate::MLError> {
|
||||
let n = src.len();
|
||||
let mut dst = stream.alloc_zeros::<half::bf16>(n).map_err(|e| {
|
||||
let mut dst = stream.alloc_zeros::<f32>(n).map_err(|e| {
|
||||
crate::MLError::ModelError(format!("clone_cuda_slice_f32 alloc: {e}"))
|
||||
})?;
|
||||
{
|
||||
let (src_ptr, _src_guard) = src.device_ptr(stream);
|
||||
let (dst_ptr, _dst_guard) = dst.device_ptr_mut(stream);
|
||||
let num_bytes = n * std::mem::size_of::<half::bf16>();
|
||||
let num_bytes = n * std::mem::size_of::<f32>();
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
cudarc::driver::result::memcpy_dtod_async(
|
||||
@@ -218,16 +220,16 @@ pub fn clone_cuda_slice_u32(
|
||||
/// Pre-uploaded GPU training data for DQN trainer.
|
||||
///
|
||||
/// Holds market features [N * 42] and target prices [N * 4] as GPU-resident
|
||||
/// `CudaSlice<half::bf16>` buffers. Portfolio features (3 dims) are computed per-bar
|
||||
/// `CudaSlice<f32>` buffers. Portfolio features (3 dims) are computed per-bar
|
||||
/// by the trainer and concatenated on-device. OFI features (8 dims from MBP-10
|
||||
/// order book) are optionally uploaded and appended to the state vector.
|
||||
pub struct DqnGpuData {
|
||||
/// Market features [num_bars * 42] on GPU (f32, row-major)
|
||||
pub features: CudaSlice<half::bf16>,
|
||||
pub features: CudaSlice<f32>,
|
||||
/// Target prices [num_bars * 4] on GPU (f32, row-major)
|
||||
pub targets: CudaSlice<half::bf16>,
|
||||
pub targets: CudaSlice<f32>,
|
||||
/// OFI features [num_bars * 8] on GPU (f32), from MBP-10 data
|
||||
pub ofi_features: Option<CudaSlice<half::bf16>>,
|
||||
pub ofi_features: Option<CudaSlice<f32>>,
|
||||
/// Number of training bars
|
||||
pub num_bars: usize,
|
||||
/// Feature dimension (42)
|
||||
@@ -415,19 +417,19 @@ impl DqnGpuData {
|
||||
|
||||
/// D2D subrange copy: extract `count` f32 elements at `offset` into a new `CudaSlice`.
|
||||
pub(crate) fn d2d_subrange(
|
||||
src: &CudaSlice<half::bf16>,
|
||||
src: &CudaSlice<f32>,
|
||||
offset: usize,
|
||||
count: usize,
|
||||
stream: &Arc<CudaStream>,
|
||||
) -> Result<CudaSlice<half::bf16>, MLError> {
|
||||
let mut dst = stream.alloc_zeros::<half::bf16>(count).map_err(|e| {
|
||||
) -> Result<CudaSlice<f32>, MLError> {
|
||||
let mut dst = stream.alloc_zeros::<f32>(count).map_err(|e| {
|
||||
MLError::ModelError(format!("d2d_subrange alloc: {e}"))
|
||||
})?;
|
||||
{
|
||||
let src_view = src.slice(offset..offset + count);
|
||||
let (src_ptr, _sg) = src_view.device_ptr(stream);
|
||||
let (dst_ptr, _dg) = dst.device_ptr_mut(stream);
|
||||
let nbytes = count * std::mem::size_of::<half::bf16>();
|
||||
let nbytes = count * std::mem::size_of::<f32>();
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
cudarc::driver::result::memcpy_dtod_async(
|
||||
@@ -439,7 +441,7 @@ impl DqnGpuData {
|
||||
}
|
||||
|
||||
/// Get market features for a single bar as a [42] `CudaSlice` (D2D copy from GPU buffer).
|
||||
pub fn bar_features(&self, bar_idx: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<half::bf16>, MLError> {
|
||||
pub fn bar_features(&self, bar_idx: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<f32>, MLError> {
|
||||
if bar_idx >= self.num_bars {
|
||||
return Err(MLError::ModelError(format!("Bar {bar_idx} out of range (num_bars={})", self.num_bars)));
|
||||
}
|
||||
@@ -447,13 +449,13 @@ impl DqnGpuData {
|
||||
}
|
||||
|
||||
/// Get market features for a batch of bars as a [batch_size * 42] `CudaSlice`.
|
||||
pub fn batch_features(&self, start: usize, count: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<half::bf16>, MLError> {
|
||||
pub fn batch_features(&self, start: usize, count: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<f32>, MLError> {
|
||||
let count = count.min(self.num_bars.saturating_sub(start));
|
||||
Self::d2d_subrange(&self.features, start * self.feature_dim, count * self.feature_dim, stream)
|
||||
}
|
||||
|
||||
/// Get target prices for a single bar as a [4] `CudaSlice`.
|
||||
pub fn bar_targets(&self, bar_idx: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<half::bf16>, MLError> {
|
||||
pub fn bar_targets(&self, bar_idx: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<f32>, MLError> {
|
||||
if bar_idx >= self.num_bars {
|
||||
return Err(MLError::ModelError(format!("Bar {bar_idx} target out of range (num_bars={})", self.num_bars)));
|
||||
}
|
||||
@@ -461,16 +463,16 @@ impl DqnGpuData {
|
||||
}
|
||||
|
||||
/// Get target prices for a batch as [batch_size * 4] `CudaSlice`.
|
||||
pub fn batch_targets(&self, start: usize, count: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<half::bf16>, MLError> {
|
||||
pub fn batch_targets(&self, start: usize, count: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<f32>, MLError> {
|
||||
let count = count.min(self.num_bars.saturating_sub(start));
|
||||
Self::d2d_subrange(&self.targets, start * 4, count * 4, stream)
|
||||
}
|
||||
|
||||
/// Get target values for a bar as a GPU-resident [4] `CudaSlice<half::bf16>`.
|
||||
/// Get target values for a bar as a GPU-resident [4] `CudaSlice<f32>`.
|
||||
///
|
||||
/// Tensor order: [current_close_preproc, next_close_preproc, current_close_raw, next_close_raw].
|
||||
/// Stays on GPU. Callers that need CPU scalars should extract at their own boundary.
|
||||
pub fn bar_target_values(&self, bar_idx: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<half::bf16>, MLError> {
|
||||
pub fn bar_target_values(&self, bar_idx: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<f32>, MLError> {
|
||||
self.bar_targets(bar_idx, stream)
|
||||
}
|
||||
|
||||
@@ -483,13 +485,13 @@ impl DqnGpuData {
|
||||
bar_idx: usize,
|
||||
portfolio_features: &[f32; 3],
|
||||
stream: &Arc<CudaStream>,
|
||||
) -> Result<CudaSlice<half::bf16>, MLError> {
|
||||
) -> Result<CudaSlice<f32>, MLError> {
|
||||
let ofi_dim = if self.ofi_features.is_some() { 8 } else { 0 };
|
||||
let raw_dim = self.feature_dim + 3 + ofi_dim;
|
||||
let final_dim = self.aligned_state_dim.unwrap_or(raw_dim);
|
||||
|
||||
// Allocate output buffer (zeros for padding region)
|
||||
let mut dst = stream.alloc_zeros::<half::bf16>(final_dim)
|
||||
let mut dst = stream.alloc_zeros::<f32>(final_dim)
|
||||
.map_err(|e| MLError::ModelError(format!("build_state alloc: {e}")))?;
|
||||
|
||||
// DtoD copy: market features -> dst[0..feature_dim]
|
||||
@@ -518,7 +520,7 @@ impl DqnGpuData {
|
||||
count: usize,
|
||||
portfolio_features: &[f32; 3],
|
||||
stream: &Arc<CudaStream>,
|
||||
) -> Result<CudaSlice<half::bf16>, MLError> {
|
||||
) -> Result<CudaSlice<f32>, MLError> {
|
||||
let count = count.min(self.num_bars.saturating_sub(start));
|
||||
if count == 0 {
|
||||
return Err(MLError::ModelError("Empty batch for state construction".to_owned()));
|
||||
@@ -528,7 +530,7 @@ impl DqnGpuData {
|
||||
let final_dim = self.aligned_state_dim.unwrap_or(raw_dim);
|
||||
|
||||
// Allocate output buffer (zeros for padding region)
|
||||
let mut dst = stream.alloc_zeros::<half::bf16>(count * final_dim)
|
||||
let mut dst = stream.alloc_zeros::<f32>(count * final_dim)
|
||||
.map_err(|e| MLError::ModelError(format!("build_batch alloc: {e}")))?;
|
||||
|
||||
// Upload portfolio features once (3 scalars)
|
||||
@@ -574,8 +576,8 @@ impl DqnGpuData {
|
||||
/// DtoD copy `count` f32 elements from `src` (offset 0) into `dst` at element offset.
|
||||
#[allow(unsafe_code)]
|
||||
fn dtod_copy_into(
|
||||
src: &CudaSlice<half::bf16>,
|
||||
dst: &mut CudaSlice<half::bf16>,
|
||||
src: &CudaSlice<f32>,
|
||||
dst: &mut CudaSlice<f32>,
|
||||
dst_offset_elems: usize,
|
||||
count: usize,
|
||||
stream: &Arc<CudaStream>,
|
||||
@@ -589,9 +591,9 @@ impl DqnGpuData {
|
||||
/// buffer. The source is sliced via `CudaSlice::slice()` for safety.
|
||||
#[allow(unsafe_code)]
|
||||
fn dtod_copy_into_at_offset(
|
||||
src: &CudaSlice<half::bf16>,
|
||||
src: &CudaSlice<f32>,
|
||||
src_offset_elems: usize,
|
||||
dst: &mut CudaSlice<half::bf16>,
|
||||
dst: &mut CudaSlice<f32>,
|
||||
dst_offset_elems: usize,
|
||||
count: usize,
|
||||
stream: &Arc<CudaStream>,
|
||||
@@ -599,12 +601,12 @@ impl DqnGpuData {
|
||||
if count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let nbytes = count * std::mem::size_of::<half::bf16>();
|
||||
let nbytes = count * std::mem::size_of::<f32>();
|
||||
let src_view = src.slice(src_offset_elems..src_offset_elems + count);
|
||||
let (src_ptr, _sg) = src_view.device_ptr(stream);
|
||||
// Get base pointer of dst, then offset by dst_offset_elems * sizeof(f32)
|
||||
let (dst_base, _dg) = dst.device_ptr_mut(stream);
|
||||
let dst_ptr = dst_base + (dst_offset_elems * std::mem::size_of::<half::bf16>()) as u64;
|
||||
let dst_ptr = dst_base + (dst_offset_elems * std::mem::size_of::<f32>()) as u64;
|
||||
unsafe {
|
||||
cudarc::driver::result::memcpy_dtod_async(
|
||||
dst_ptr, src_ptr, nbytes, stream.cu_stream(),
|
||||
@@ -620,7 +622,7 @@ impl DqnGpuData {
|
||||
#[allow(unsafe_code)]
|
||||
fn htod_copy_into(
|
||||
src: &[f32],
|
||||
dst: &mut CudaSlice<half::bf16>,
|
||||
dst: &mut CudaSlice<f32>,
|
||||
dst_offset_elems: usize,
|
||||
stream: &Arc<CudaStream>,
|
||||
) -> Result<(), MLError> {
|
||||
@@ -709,7 +711,7 @@ impl GpuBufferPool {
|
||||
}
|
||||
}
|
||||
|
||||
// Upload the used slice to GPU via f32→bf16 conversion + clone_htod.
|
||||
// Upload the used slice to GPU via clone_htod.
|
||||
let features = clone_htod_f32_to_bf16(stream, &self.feature_buf[..feat_len])?;
|
||||
let targets = clone_htod_f32_to_bf16(stream, &self.target_buf[..targ_len])?;
|
||||
|
||||
@@ -731,10 +733,10 @@ impl GpuBufferPool {
|
||||
|
||||
/// Pre-uploaded GPU training data for PPO trainer.
|
||||
///
|
||||
/// Holds all rollout states as a single [N * state_dim] `CudaSlice<half::bf16>`.
|
||||
/// Holds all rollout states as a single [N * state_dim] `CudaSlice<f32>`.
|
||||
pub struct PpoGpuData {
|
||||
/// All states [num_steps * state_dim] on GPU (f32, row-major)
|
||||
pub states: CudaSlice<half::bf16>,
|
||||
pub states: CudaSlice<f32>,
|
||||
/// Number of training steps
|
||||
pub num_steps: usize,
|
||||
/// State dimension
|
||||
@@ -751,7 +753,7 @@ impl std::fmt::Debug for PpoGpuData {
|
||||
}
|
||||
|
||||
impl PpoGpuData {
|
||||
/// Upload PPO market data to GPU as a single contiguous `CudaSlice<half::bf16>`.
|
||||
/// Upload PPO market data to GPU as a single contiguous `CudaSlice<f32>`.
|
||||
pub fn upload(market_data: &[Vec<f32>], stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
||||
let num_steps = market_data.len();
|
||||
if num_steps == 0 {
|
||||
@@ -796,7 +798,7 @@ impl PpoGpuData {
|
||||
}
|
||||
|
||||
/// Get a single step's state as a [state_dim] `CudaSlice` (D2D copy from GPU buffer).
|
||||
pub fn step_state(&self, step_idx: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<half::bf16>, MLError> {
|
||||
pub fn step_state(&self, step_idx: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<f32>, MLError> {
|
||||
if step_idx >= self.num_steps {
|
||||
return Err(MLError::ModelError(format!("Step {step_idx} out of range (num_steps={})", self.num_steps)));
|
||||
}
|
||||
@@ -804,7 +806,7 @@ impl PpoGpuData {
|
||||
}
|
||||
|
||||
/// Get a batch of states as [batch_size * state_dim] `CudaSlice`.
|
||||
pub fn batch_states(&self, start: usize, count: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<half::bf16>, MLError> {
|
||||
pub fn batch_states(&self, start: usize, count: usize, stream: &Arc<CudaStream>) -> Result<CudaSlice<f32>, MLError> {
|
||||
let count = count.min(self.num_steps.saturating_sub(start));
|
||||
DqnGpuData::d2d_subrange(&self.states, start * self.state_dim, count * self.state_dim, stream)
|
||||
}
|
||||
@@ -850,11 +852,10 @@ mod tests {
|
||||
let batch = gpu_data.batch_features(0, 10, &stream).expect("batch");
|
||||
assert_eq!(batch.len(), 10 * 42);
|
||||
|
||||
// Download target values and verify on CPU (bf16 -> f32 conversion)
|
||||
// Download target values and verify on CPU
|
||||
let targets_gpu = gpu_data.bar_target_values(0, &stream).expect("targets");
|
||||
let mut targets_bf16 = vec![half::bf16::ZERO; 4];
|
||||
stream.memcpy_dtoh(&targets_gpu, &mut targets_bf16).expect("DtoH"); // test readback
|
||||
let targets_host: Vec<f32> = targets_bf16.iter().map(|v| v.to_f32()).collect();
|
||||
let mut targets_host = vec![0.0_f32; 4];
|
||||
dtoh_f32(&stream, &targets_gpu, &mut targets_host).expect("DtoH");
|
||||
assert!((targets_host.first().copied().unwrap_or(0.0) - 100.0).abs() < 0.5);
|
||||
}
|
||||
|
||||
@@ -962,10 +963,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_vram_estimation() {
|
||||
// 300K bars × 55 features × 2 bytes (BF16) = ~33 MB
|
||||
// 300K bars × 55 features × 4 bytes (f32) = ~66 MB
|
||||
let bytes = estimate_vram_bytes(300_000 * 55);
|
||||
assert!(bytes < 50_000_000); // Under 50 MB
|
||||
assert!(bytes > 30_000_000); // Over 30 MB
|
||||
assert!(bytes < 100_000_000); // Under 100 MB
|
||||
assert!(bytes > 60_000_000); // Over 60 MB
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1001,13 +1002,11 @@ mod tests {
|
||||
let batch = gpu_data.build_batch_states(10, 32, &portfolio, &stream).expect("batch");
|
||||
assert_eq!(batch.len(), 32 * 45); // 42 market + 3 portfolio
|
||||
|
||||
// Download and verify portfolio features in row 0 (bf16 -> f32 conversion)
|
||||
let mut host_bf16 = vec![half::bf16::ZERO; 32 * 45];
|
||||
stream.memcpy_dtoh(&batch, &mut host_bf16).expect("DtoH"); // test readback
|
||||
let host: Vec<f32> = host_bf16.iter().map(|v| v.to_f32()).collect();
|
||||
// Download and verify portfolio features in row 0
|
||||
let mut host = vec![0.0_f32; 32 * 45];
|
||||
dtoh_f32(&stream, &batch, &mut host).expect("DtoH");
|
||||
|
||||
// Row 0: features[42..45] should be portfolio [0.95, 0.5, 0.0001]
|
||||
// BF16 precision: relax tolerance
|
||||
let pf = &host[42..45];
|
||||
assert!((pf[0] - 0.95).abs() < 0.02, "portfolio[0]={}", pf[0]);
|
||||
assert!((pf[1] - 0.5).abs() < 0.02, "portfolio[1]={}", pf[1]);
|
||||
@@ -1072,11 +1071,10 @@ mod tests {
|
||||
let g2 = pool.upload_dqn(&data2, &stream).expect("upload2");
|
||||
assert_eq!(g2.num_bars, 200);
|
||||
|
||||
// Download target values and verify (bf16 -> f32 conversion)
|
||||
// Download target values and verify
|
||||
let t = g2.bar_target_values(0, &stream).expect("targets");
|
||||
let mut host_bf16 = vec![half::bf16::ZERO; 4];
|
||||
stream.memcpy_dtoh(&t, &mut host_bf16).expect("DtoH"); // test readback
|
||||
let host: Vec<f32> = host_bf16.iter().map(|v| v.to_f32()).collect();
|
||||
let mut host = vec![0.0_f32; 4];
|
||||
dtoh_f32(&stream, &t, &mut host).expect("DtoH");
|
||||
assert!((host[0] - 200.0).abs() < 1.0);
|
||||
}
|
||||
|
||||
@@ -1231,17 +1229,14 @@ mod tests {
|
||||
let advantages_host: Vec<f32> = (0..total).map(|i| i as f32 * 0.1 - 2.0).collect();
|
||||
let returns_host: Vec<f32> = (0..total).map(|i| i as f32 * 0.5).collect();
|
||||
|
||||
let states_bf16: Vec<half::bf16> = states_host.iter().map(|&v| half::bf16::from_f32(v)).collect();
|
||||
let log_probs_bf16: Vec<half::bf16> = vec![half::bf16::from_f32(-1.5); total];
|
||||
let advantages_bf16: Vec<half::bf16> = advantages_host.iter().map(|&v| half::bf16::from_f32(v)).collect();
|
||||
let returns_bf16: Vec<half::bf16> = returns_host.iter().map(|&v| half::bf16::from_f32(v)).collect();
|
||||
let log_probs_host: Vec<f32> = vec![-1.5_f32; total];
|
||||
|
||||
let batch = PpoExperienceBatch {
|
||||
states: stream.clone_htod(&states_bf16).unwrap(),
|
||||
states: stream.clone_htod(&states_host).unwrap(),
|
||||
actions: stream.clone_htod(&(0..total).map(|i| (i % 45) as i32).collect::<Vec<_>>()).unwrap(),
|
||||
log_probs: stream.clone_htod(&log_probs_bf16).unwrap(),
|
||||
advantages: stream.clone_htod(&advantages_bf16).unwrap(),
|
||||
returns: stream.clone_htod(&returns_bf16).unwrap(),
|
||||
log_probs: stream.clone_htod(&log_probs_host).unwrap(),
|
||||
advantages: stream.clone_htod(&advantages_host).unwrap(),
|
||||
returns: stream.clone_htod(&returns_host).unwrap(),
|
||||
done_flags: stream.clone_htod(&(0..total).map(|i| if i % timesteps == timesteps - 1 { 1_i32 } else { 0 }).collect::<Vec<_>>()).unwrap(),
|
||||
n_episodes,
|
||||
timesteps,
|
||||
@@ -1322,17 +1317,15 @@ mod tests {
|
||||
}
|
||||
let returns_host: Vec<f32> = advantages_host.iter().map(|&a| a + 5.0).collect();
|
||||
|
||||
let states_bf16: Vec<half::bf16> = vec![half::bf16::ZERO; total * state_dim];
|
||||
let log_probs_bf16: Vec<half::bf16> = vec![half::bf16::from_f32(-1.0); total];
|
||||
let advantages_bf16: Vec<half::bf16> = advantages_host.iter().map(|&v| half::bf16::from_f32(v)).collect();
|
||||
let returns_bf16: Vec<half::bf16> = returns_host.iter().map(|&v| half::bf16::from_f32(v)).collect();
|
||||
let states_f32: Vec<f32> = vec![0.0_f32; total * state_dim];
|
||||
let log_probs_f32: Vec<f32> = vec![-1.0_f32; total];
|
||||
|
||||
let batch = PpoExperienceBatch {
|
||||
states: stream.clone_htod(&states_bf16).unwrap(),
|
||||
states: stream.clone_htod(&states_f32).unwrap(),
|
||||
actions: stream.clone_htod(&vec![1_i32; total]).unwrap(),
|
||||
log_probs: stream.clone_htod(&log_probs_bf16).unwrap(),
|
||||
advantages: stream.clone_htod(&advantages_bf16).unwrap(),
|
||||
returns: stream.clone_htod(&returns_bf16).unwrap(),
|
||||
log_probs: stream.clone_htod(&log_probs_f32).unwrap(),
|
||||
advantages: stream.clone_htod(&advantages_host).unwrap(),
|
||||
returns: stream.clone_htod(&returns_host).unwrap(),
|
||||
done_flags: stream.clone_htod(&(0..total).map(|i| if i % timesteps == timesteps - 1 { 1_i32 } else { 0 }).collect::<Vec<_>>()).unwrap(),
|
||||
n_episodes,
|
||||
timesteps,
|
||||
@@ -1347,7 +1340,7 @@ mod tests {
|
||||
let r = returns.get(i).copied().unwrap_or(0.0);
|
||||
let a = advantages.get(i).copied().unwrap_or(0.0);
|
||||
assert!(
|
||||
(r - a - 5.0).abs() < 0.1, // BF16 precision: values round-trip with ~1% error
|
||||
(r - a - 5.0).abs() < 0.1, // f32 precision: values round-trip cleanly
|
||||
"Value at {} should be ~5.0, got {}",
|
||||
i,
|
||||
r - a
|
||||
|
||||
Reference in New Issue
Block a user