use super::{
AdamWOptions, AdamWState, AdamWUpdate, FusedAdamWError, StepControl,
device::{adamw_step_scaled, validate_step, validate_tensor},
gradient_norm::{GradientGuardError, GradientGuardOptions, GradientStats},
guard_kernel,
};
use ruda_core::{device::Device, future::block_on, tensor::DType};
use ruda_kernel::{
dsl::{Runtime, prelude::{RudaCount, RudaDim}, server::CopyDescriptor},
tensor::{RudaTensor, allocation::empty_device_contiguous_dtype},
};
const THREADS: u32 = 256;
const MAX_BLOCKS: usize = 1024;
const ITEMS_PER_BLOCK: usize = 1024;
const SHARED_BYTES: usize = THREADS as usize * 12;
#[derive(Debug, Clone, PartialEq)]
pub struct GradientStatsReport {
pub stats: GradientStats,
pub reduction_launches: usize,
pub readback_bytes: usize,
pub scratch_bytes: usize,
}
pub struct AdamWEntry<'a, R: Runtime> {
pub parameters: &'a RudaTensor<R>,
pub gradients: &'a RudaTensor<R>,
pub state: Option<&'a AdamWState<R>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SkipReason {
Requested,
Empty,
NonFinite,
}
pub struct GuardedAdamWUpdate<R: Runtime> {
pub updates: Vec<AdamWUpdate<R>>,
pub statistics: Option<GradientStatsReport>,
pub clip_multiplier: f32,
pub skip_reason: Option<SkipReason>,
}
fn same_queue<R: Runtime>(a: &RudaTensor<R>, b: &RudaTensor<R>) -> bool {
a.device.to_id() == b.device.to_id() && a.client.same_execution_queue(&b.client)
}
fn preflight_stats<R: Runtime>(gradients: &[&RudaTensor<R>]) -> Result<Vec<usize>, GradientGuardError> {
let mut sizes = Vec::with_capacity(gradients.len());
for &g in gradients {
let size = validate_tensor(g, true)?;
if let Some(&first) = gradients.first() {
if !same_queue(first, g) {
return Err(FusedAdamWError::DifferentExecutionQueue("gradient group").into());
}
}
if size > 0 {
let h = &g.client.properties().hardware;
if h.max_units_per_ruda < THREADS || h.max_ruda_dim.0 < THREADS
|| h.max_ruda_count.0 == 0 || h.max_shared_memory_size < SHARED_BYTES
{
return Err(GradientGuardError::UnsupportedDevice("requires 256 X threads and 3072 shared bytes"));
}
}
sizes.push(size);
}
Ok(sizes)
}
pub fn gradient_stats_sync<R: Runtime>(
gradients: &[&RudaTensor<R>], gradient_scale: f32,
) -> Result<GradientStatsReport, GradientGuardError> {
let mut stats = GradientStats::empty(gradient_scale)?;
let sizes = preflight_stats(gradients)?;
let mut pending = Vec::new();
let mut launches = 0;
let mut scratch_bytes = 0;
for (&g, &size) in gradients.iter().zip(&sizes) {
if size == 0 {
stats.add_device_summary(&[0.0, 0.0, 0.0], 0)?;
continue;
}
let blocks = size.div_ceil(ITEMS_PER_BLOCK).min(MAX_BLOCKS)
.min(g.client.properties().hardware.max_ruda_count.0 as usize);
let make = |elements| empty_device_contiguous_dtype(
g.client.clone(), g.device.clone(), [elements].into(), DType::F32,
);
let partials = make(blocks * 3);
let dim = RudaDim::new_1d(THREADS);
guard_kernel::scaled_l2::launch::<R>(
&g.client, RudaCount::new_1d(blocks as u32), dim,
(*g).clone().into_array_arg(), partials.clone().into_array_arg(),
gradient_scale.recip(), false, include_str!("guard_kernel.rs").to_owned(), g.dtype.into(),
);
launches += 1;
scratch_bytes += blocks * 12;
let summary = if blocks == 1 { partials } else {
let summary = make(3);
guard_kernel::scaled_l2::launch::<R>(
&g.client, RudaCount::new_single(), dim,
partials.into_array_arg(), summary.clone().into_array_arg(),
1.0, true, include_str!("guard_kernel.rs").to_owned(), DType::F32.into(),
);
launches += 1;
scratch_bytes += 12;
summary
};
pending.push((summary, size));
}
if let Some((first, _)) = pending.first() {
let descriptors = pending.iter().map(|(t, _)| CopyDescriptor::new(
t.handle.clone().binding(), t.meta.shape().clone(), t.meta.strides().clone(), 4,
)).collect();
let bytes = block_on(first.client.read_tensor_async(descriptors))
.map_err(|e| GradientGuardError::Readback(format!("{e}")))?;
if bytes.len() != pending.len() {
return Err(GradientGuardError::InvalidSummary("missing readback tensors"));
}
for (raw, (_, size)) in bytes.into_iter().zip(&pending) {
if raw.len() != 12 { return Err(GradientGuardError::InvalidSummary("summary must be 12 bytes")); }
let mut triple = [0.0f32; 3];
for (dst, chunk) in triple.iter_mut().zip(raw.chunks_exact(4)) {
*dst = f32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
stats.add_device_summary(&triple, *size)?;
}
}
Ok(GradientStatsReport { stats, reduction_launches: launches, readback_bytes: pending.len() * 12, scratch_bytes })
}
pub fn guarded_adamw_step<R: Runtime>(
entries: &[AdamWEntry<'_, R>], options: &AdamWOptions,
control: StepControl, guard: GradientGuardOptions,
) -> Result<GuardedAdamWUpdate<R>, GradientGuardError> {
options.validate()?;
control.validate()?;
guard.validate()?;
let mut nonempty = false;
for entry in entries {
let size = validate_step(entry.parameters, entry.gradients, entry.state, options, control)?;
if let Some(first) = entries.first() {
if !same_queue(first.parameters, entry.parameters) {
return Err(FusedAdamWError::DifferentExecutionQueue("optimizer group").into());
}
}
nonempty |= size != 0;
}
let unchanged = |reason, statistics| GuardedAdamWUpdate {
updates: entries.iter().map(|e| AdamWUpdate {
parameters: e.parameters.clone(), state: e.state.cloned(), updated: false,
}).collect(),
statistics, clip_multiplier: 1.0, skip_reason: Some(reason),
};
if control.skip_update { return Ok(unchanged(SkipReason::Requested, None)); }
if !nonempty { return Ok(unchanged(SkipReason::Empty, None)); }
let gradients: Vec<_> = entries.iter().map(|e| e.gradients).collect();
let statistics = gradient_stats_sync(&gradients, control.gradient_scale)?;
let decision = statistics.stats.decision(guard)?;
if decision.skip_update { return Ok(unchanged(SkipReason::NonFinite, Some(statistics))); }
let updates = entries.iter().map(|e| adamw_step_scaled(
e.parameters, e.gradients, e.state, options, control, decision.clip_multiplier,
)).collect::<Result<Vec<_>, _>>()?;
Ok(GuardedAdamWUpdate {
updates, statistics: Some(statistics), clip_multiplier: decision.clip_multiplier, skip_reason: None,
})
}