optirs_gpu/
mixed_precision.rs1#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct MixedPrecisionConfig {
13 pub init_scale: f32,
15 pub growth_factor: f32,
17 pub backoff_factor: f32,
19 pub growth_interval: u32,
21 pub min_scale: f32,
23 pub max_scale: f32,
25 pub use_bfloat16: bool,
27}
28
29impl Default for MixedPrecisionConfig {
30 fn default() -> Self {
31 Self {
32 init_scale: 65536.0,
33 growth_factor: 2.0,
34 backoff_factor: 0.5,
35 growth_interval: 2000,
36 min_scale: 1.0,
37 max_scale: 65536.0 * 128.0,
38 use_bfloat16: false,
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct OverflowStats {
46 pub total_steps: usize,
48 pub overflow_count: usize,
50 pub overflow_rate: f32,
52 pub current_scale: f32,
54}
55
56#[derive(Debug, Clone)]
58pub struct DynamicLossScaler {
59 scale: f32,
60 config: MixedPrecisionConfig,
61 growth_tracker: u32,
62 window: Vec<bool>,
63}
64
65impl DynamicLossScaler {
66 pub const WINDOW: usize = 100;
68
69 pub fn new(config: MixedPrecisionConfig) -> Self {
71 Self {
72 scale: config.init_scale.clamp(config.min_scale, config.max_scale),
73 config,
74 growth_tracker: 0,
75 window: Vec::with_capacity(Self::WINDOW),
76 }
77 }
78
79 pub fn scale(&self) -> f32 {
81 self.scale
82 }
83
84 pub fn inv_scale(&self) -> f32 {
86 1.0 / self.scale
87 }
88
89 pub fn update(&mut self, has_overflow: bool) {
91 if self.window.len() == Self::WINDOW {
92 self.window.remove(0);
93 }
94 self.window.push(has_overflow);
95
96 if has_overflow {
97 self.scale = (self.scale * self.config.backoff_factor).max(self.config.min_scale);
98 self.growth_tracker = 0;
99 } else {
100 self.growth_tracker = self.growth_tracker.saturating_add(1);
101 if self.growth_tracker >= self.config.growth_interval {
102 self.scale = (self.scale * self.config.growth_factor).min(self.config.max_scale);
103 self.growth_tracker = 0;
104 }
105 }
106 }
107
108 pub fn overflow_stats(&self) -> OverflowStats {
110 let total = self.window.len();
111 let overflows = self.window.iter().filter(|&&x| x).count();
112 OverflowStats {
113 total_steps: total,
114 overflow_count: overflows,
115 overflow_rate: if total > 0 {
116 overflows as f32 / total as f32
117 } else {
118 0.0
119 },
120 current_scale: self.scale,
121 }
122 }
123
124 pub fn unscale_and_check(&mut self, values: &mut [f32]) -> bool {
130 let inv = self.inv_scale();
131 let mut overflow = false;
132 for v in values.iter_mut() {
133 if !v.is_finite() {
134 overflow = true;
135 }
136 *v *= inv;
137 }
138 self.update(overflow);
139 overflow
140 }
141}
142
143pub fn f32_to_f16_bits(value: f32) -> u16 {
150 let bits = value.to_bits();
151 let sign = ((bits >> 16) & 0x8000) as u16;
152 let exponent = ((bits >> 23) & 0xff) as i32;
153 let mantissa = bits & 0x007f_ffff;
154
155 if exponent == 0xff {
156 return if mantissa == 0 {
158 sign | 0x7c00
159 } else {
160 sign | 0x7c00 | ((mantissa >> 13) as u16) | 0x0200
162 };
163 }
164
165 let unbiased = exponent - 127;
167 let half_exp = unbiased + 15;
168
169 if half_exp >= 0x1f {
170 return sign | 0x7c00;
172 }
173
174 if half_exp <= 0 {
175 if half_exp < -10 {
178 return sign;
179 }
180 let significand = mantissa | 0x0080_0000;
181 let shift = (14 - half_exp) as u32; let result = significand >> shift;
183 let round_bit = 1u32 << (shift - 1);
185 let remainder = significand & (round_bit.saturating_mul(2) - 1);
186 let mut half = result as u16;
187 if remainder > round_bit || (remainder == round_bit && (result & 1) == 1) {
188 half = half.wrapping_add(1);
189 }
190 return sign | half;
191 }
192
193 let mut half = ((half_exp as u16) << 10) | ((mantissa >> 13) as u16);
195 let remainder = mantissa & 0x1fff;
196 if remainder > 0x1000 || (remainder == 0x1000 && (half & 1) == 1) {
197 half = half.wrapping_add(1);
201 }
202 sign | half
203}
204
205pub fn f16_bits_to_f32(bits: u16) -> f32 {
207 let sign = ((bits as u32) & 0x8000) << 16;
208 let exponent = ((bits >> 10) & 0x1f) as u32;
209 let mantissa = ((bits & 0x03ff) as u32) << 13;
210
211 if exponent == 0 {
212 if mantissa == 0 {
213 return f32::from_bits(sign);
214 }
215 let mut mant = mantissa;
220 let mut shifts: i32 = 0;
221 while mant & 0x0080_0000 == 0 {
222 mant <<= 1;
223 shifts += 1;
224 }
225 mant &= 0x007f_ffff;
226 let f32_exp = ((113 - shifts) as u32) << 23;
227 return f32::from_bits(sign | f32_exp | mant);
228 }
229
230 if exponent == 0x1f {
231 return f32::from_bits(sign | 0x7f80_0000 | mantissa);
233 }
234
235 let f32_exp = (exponent + (127 - 15)) << 23;
236 f32::from_bits(sign | f32_exp | mantissa)
237}
238
239pub fn f32_slice_to_f16_bits(values: &[f32]) -> Vec<u16> {
241 values.iter().copied().map(f32_to_f16_bits).collect()
242}
243
244pub fn f16_bits_slice_to_f32(bits: &[u16]) -> Vec<f32> {
246 bits.iter().copied().map(f16_bits_to_f32).collect()
247}
248
249pub const F16_MAX: f32 = 65504.0;
251
252pub fn saturate_to_f16_range(value: f32) -> f32 {
254 if value.is_nan() {
255 value
256 } else {
257 value.clamp(-F16_MAX, F16_MAX)
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn round_trips_exact_values() {
267 for &v in &[
268 0.0f32, -0.0, 1.0, -1.0, 0.5, 2.0, 65504.0, -65504.0, 0.125, 1024.0,
269 ] {
270 let back = f16_bits_to_f32(f32_to_f16_bits(v));
271 assert_eq!(back.to_bits(), v.to_bits(), "value {v} did not round-trip");
272 }
273 }
274
275 #[test]
276 fn round_trips_every_finite_f16() {
277 for bits in 0u16..=u16::MAX {
279 let exponent = (bits >> 10) & 0x1f;
280 let value = f16_bits_to_f32(bits);
281 if exponent == 0x1f {
282 let back = f32_to_f16_bits(value);
284 assert_eq!(
285 (back >> 10) & 0x1f,
286 0x1f,
287 "bits {bits:#06x} lost its inf/NaN class"
288 );
289 assert_eq!(back & 0x8000, bits & 0x8000, "bits {bits:#06x} lost sign");
290 continue;
291 }
292 let back = f32_to_f16_bits(value);
293 assert_eq!(
294 back, bits,
295 "bits {bits:#06x} did not round-trip (f32 {value})"
296 );
297 }
298 }
299
300 #[test]
301 fn subnormals_are_not_flushed_to_zero() {
302 let smallest = f16_bits_to_f32(1);
304 assert!(smallest > 0.0);
305 assert!((smallest - 2f32.powi(-24)).abs() < f32::EPSILON * smallest);
306 assert_eq!(f32_to_f16_bits(smallest), 1);
307 }
308
309 #[test]
310 fn rounds_half_to_even() {
311 let halfway = 1.0f32 + 2f32.powi(-11);
314 assert_eq!(f32_to_f16_bits(halfway), f32_to_f16_bits(1.0));
315 let halfway_up = 1.0f32 + 3.0 * 2f32.powi(-11);
318 assert_eq!(f32_to_f16_bits(halfway_up), f32_to_f16_bits(1.0) + 2);
319 }
320
321 #[test]
322 fn overflow_saturates_to_infinity() {
323 assert_eq!(f32_to_f16_bits(1.0e30), 0x7c00);
324 assert_eq!(f32_to_f16_bits(-1.0e30), 0xfc00);
325 assert!(f16_bits_to_f32(0x7c00).is_infinite());
326 }
327
328 #[test]
329 fn nan_stays_nan() {
330 assert!(f16_bits_to_f32(f32_to_f16_bits(f32::NAN)).is_nan());
331 }
332
333 #[test]
334 fn loss_scaler_backs_off_and_grows() {
335 let config = MixedPrecisionConfig {
336 growth_interval: 4,
337 ..MixedPrecisionConfig::default()
338 };
339 let mut scaler = DynamicLossScaler::new(config);
340 assert_eq!(scaler.scale(), 65536.0);
341
342 scaler.update(true);
343 assert_eq!(scaler.scale(), 32768.0);
344
345 for _ in 0..4 {
346 scaler.update(false);
347 }
348 assert_eq!(scaler.scale(), 65536.0);
349 }
350
351 #[test]
352 fn loss_scaler_detects_overflow_while_unscaling() {
353 let mut scaler = DynamicLossScaler::new(MixedPrecisionConfig {
354 init_scale: 4.0,
355 ..MixedPrecisionConfig::default()
356 });
357 let mut grads = [8.0f32, -4.0, 2.0];
358 assert!(!scaler.unscale_and_check(&mut grads));
359 assert_eq!(grads, [2.0, -1.0, 0.5]);
360 assert_eq!(scaler.scale(), 4.0);
361
362 let mut bad = [f32::INFINITY, 1.0];
363 assert!(scaler.unscale_and_check(&mut bad));
364 assert_eq!(scaler.scale(), 2.0);
365
366 let stats = scaler.overflow_stats();
367 assert_eq!(stats.total_steps, 2);
368 assert_eq!(stats.overflow_count, 1);
369 }
370
371 #[test]
372 fn saturation_keeps_values_finite() {
373 assert_eq!(saturate_to_f16_range(1.0e30), F16_MAX);
374 assert_eq!(saturate_to_f16_range(-1.0e30), -F16_MAX);
375 assert!(saturate_to_f16_range(f32::NAN).is_nan());
376 }
377}