// Weight Update WGSL Kernel (AdamW Optimizer) // Implements fused AdamW parameter update on the GPU. // // AdamW update rule: // m_t = beta1 * m_{t-1} + (1 - beta1) * g_t // v_t = beta2 * v_{t-1} + (1 - beta2) * g_t^2 // m_hat = m_t / (1 - beta1^t) // v_hat = v_t / (1 - beta2^t) // theta_t = theta_{t-1} * (1 - lr * weight_decay) - lr * m_hat / (sqrt(v_hat) + eps) export const WEIGHT_UPDATE_WGSL: string = /* wgsl */` struct AdamParams { num_elements : u32, lr : f32, // learning rate beta1 : f32, // default 0.9 beta2 : f32, // default 0.999 eps : f32, // default 1e-8 weight_decay : f32, // default 0.01 beta1_t : f32, // beta1^t (precomputed bias correction term) beta2_t : f32, // beta2^t max_delta : f32, // trust region: max |Δθ| per step (0 ⇒ unbounded) _pad0 : f32, // pad struct to a 16-byte multiple (uniform layout) _pad1 : f32, _pad2 : f32, }; @group(0) @binding(0) var adam : AdamParams; // param (N,) – weight tensor (read-write: updated in-place) @group(0) @binding(1) var param : array; // grad (N,) – gradient @group(0) @binding(2) var grad : array; // m (N,) – first moment @group(0) @binding(3) var m_state : array; // v (N,) – second moment @group(0) @binding(4) var v_state : array; // Dispatch: (ceil(N / 256), 1, 1) @compute @workgroup_size(256, 1, 1) fn adamw_update( @builtin(global_invocation_id) gid : vec3, ) { let i = gid.x; if (i >= adam.num_elements) { return; } let g = grad[i]; let p = param[i]; // Moment updates let m_new = adam.beta1 * m_state[i] + (1.0 - adam.beta1) * g; let v_new = adam.beta2 * v_state[i] + (1.0 - adam.beta2) * g * g; m_state[i] = m_new; v_state[i] = v_new; // Bias-corrected estimates let m_hat = m_new / (1.0 - adam.beta1_t); let v_hat = v_new / (1.0 - adam.beta2_t); // Adam step. var step = adam.lr * m_hat / (sqrt(v_hat) + adam.eps); // Numerical guard: never write a non-finite step into a weight. A NaN or Inf // here (from a bad gradient, a zero v_hat, an overflow) would permanently // poison the parameter and every future forward — the "model dies" failure. // NaN fails self-comparison; treat ±Inf-magnitude as non-finite too. if (step != step || step > 3.4e38 || step < -3.4e38) { step = 0.0; } // Trust region: bound how far ONE step can move a weight. With write-through // adaptation running repeatedly, an unbounded step (even from a noisy // gradient) compounds across executions and blows the weights up; clamping // the per-element delta keeps every adapt small and reversible. max_delta==0 // disables the bound (full-training callers that don't set it). if (adam.max_delta > 0.0) { step = clamp(step, -adam.max_delta, adam.max_delta); } // Weight decay (decoupled) + bounded gradient step param[i] = p * (1.0 - adam.lr * adam.weight_decay) - step; } `; // Gradient clipping kernel – clips global gradient norm to max_norm. // Run before weight updates. Two-pass: first compute squared norm, then scale. export const GRAD_CLIP_WGSL: string = /* wgsl */` struct ClipParams { num_elements : u32, max_norm_sq : f32, // max_norm^2 }; @group(0) @binding(0) var clip_p : ClipParams; @group(0) @binding(1) var grad : array; @group(0) @binding(2) var norm_sq : array; // size 1, atomic accumulator var local_sq : array; // Pass 1: reduce sum of squares into norm_sq[0] @compute @workgroup_size(256, 1, 1) fn grad_norm_reduce( @builtin(global_invocation_id) gid : vec3, @builtin(local_invocation_index) lid : u32, ) { let i = gid.x; local_sq[lid] = 0.0; if (i < clip_p.num_elements) { local_sq[lid] = grad[i] * grad[i]; } workgroupBarrier(); // Parallel reduction within workgroup var s: u32 = 128u; loop { if (s == 0u) { break; } if (lid < s) { local_sq[lid] = local_sq[lid] + local_sq[lid + s]; } workgroupBarrier(); s = s >> 1u; } if (lid == 0u) { // Non-atomic accumulation (single workgroup assumption for small models) norm_sq[0] = norm_sq[0] + local_sq[0]; } } // Pass 2: scale gradients if norm exceeds max_norm @compute @workgroup_size(256, 1, 1) fn grad_clip_scale( @builtin(global_invocation_id) gid : vec3, ) { let i = gid.x; if (i >= clip_p.num_elements) { return; } let ns = norm_sq[0]; if (ns > clip_p.max_norm_sq) { let scale = sqrt(clip_p.max_norm_sq / ns); grad[i] = grad[i] * scale; } } `;