/home/noah/src/realizar/src/bench/gpu_parity.rs
Line | Count | Source |
1 | | //! GPU parity benchmarks for comparing Realizar vs Ollama/llama.cpp |
2 | | //! |
3 | | //! Extracted from bench/mod.rs (PMAT-802) to reduce module size. |
4 | | //! Contains: |
5 | | //! - IMP-800: TRUE GPU Parity Benchmark (M2 Milestone) |
6 | | //! - IMP-900: Closing the 18x Gap (M3/M4 Milestones) |
7 | | |
8 | | use serde::{Deserialize, Serialize}; |
9 | | |
10 | | // ============================================================================ |
11 | | // IMP-800: TRUE GPU Parity Benchmark (M2 Milestone) |
12 | | // ============================================================================ |
13 | | |
14 | | /// GPU parity benchmark configuration (IMP-800b) |
15 | | /// |
16 | | /// Configures apples-to-apples throughput comparison on same GPU. |
17 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
18 | | pub struct GpuParityBenchmark { |
19 | | /// Model to benchmark (phi-2 Q4_K_M) |
20 | | pub model_path: String, |
21 | | /// Prompt for generation |
22 | | pub prompt: String, |
23 | | /// Number of tokens to generate |
24 | | pub max_tokens: usize, |
25 | | /// Ollama endpoint for comparison |
26 | | pub ollama_endpoint: String, |
27 | | /// Number of warmup iterations |
28 | | pub warmup_iterations: usize, |
29 | | /// Number of measurement iterations |
30 | | pub measurement_iterations: usize, |
31 | | /// Target CV for stable measurements |
32 | | pub target_cv: f64, |
33 | | } |
34 | | |
35 | | impl Default for GpuParityBenchmark { |
36 | 2 | fn default() -> Self { |
37 | 2 | Self { |
38 | 2 | model_path: String::new(), |
39 | 2 | prompt: "The quick brown fox".to_string(), |
40 | 2 | max_tokens: 32, |
41 | 2 | ollama_endpoint: "http://localhost:11434".to_string(), |
42 | 2 | warmup_iterations: 3, |
43 | 2 | measurement_iterations: 10, |
44 | 2 | target_cv: 0.05, |
45 | 2 | } |
46 | 2 | } |
47 | | } |
48 | | |
49 | | impl GpuParityBenchmark { |
50 | | /// Create a new GPU parity benchmark with model path |
51 | | #[must_use] |
52 | 1 | pub fn new(model_path: impl Into<String>) -> Self { |
53 | 1 | Self { |
54 | 1 | model_path: model_path.into(), |
55 | 1 | ..Default::default() |
56 | 1 | } |
57 | 1 | } |
58 | | |
59 | | /// Set the prompt for generation |
60 | | #[must_use] |
61 | 1 | pub fn with_prompt(mut self, prompt: impl Into<String>) -> Self { |
62 | 1 | self.prompt = prompt.into(); |
63 | 1 | self |
64 | 1 | } |
65 | | |
66 | | /// Set the number of tokens to generate |
67 | | #[must_use] |
68 | 1 | pub fn with_max_tokens(mut self, max_tokens: usize) -> Self { |
69 | 1 | self.max_tokens = max_tokens; |
70 | 1 | self |
71 | 1 | } |
72 | | |
73 | | /// Set the Ollama endpoint |
74 | | #[must_use] |
75 | 1 | pub fn with_ollama_endpoint(mut self, endpoint: impl Into<String>) -> Self { |
76 | 1 | self.ollama_endpoint = endpoint.into(); |
77 | 1 | self |
78 | 1 | } |
79 | | |
80 | | /// Set the number of warmup iterations |
81 | | #[must_use] |
82 | 1 | pub fn with_warmup(mut self, warmup: usize) -> Self { |
83 | 1 | self.warmup_iterations = warmup; |
84 | 1 | self |
85 | 1 | } |
86 | | |
87 | | /// Set the number of measurement iterations |
88 | | #[must_use] |
89 | 1 | pub fn with_iterations(mut self, iterations: usize) -> Self { |
90 | 1 | self.measurement_iterations = iterations; |
91 | 1 | self |
92 | 1 | } |
93 | | } |
94 | | |
95 | | /// Benchmark result with statistical analysis (IMP-800b) |
96 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
97 | | pub struct GpuParityResult { |
98 | | /// Realizar GPU throughput (tok/s) |
99 | | pub realizar_gpu_tps: f64, |
100 | | /// Ollama throughput (tok/s) |
101 | | pub ollama_tps: f64, |
102 | | /// Performance gap ratio (Ollama / Realizar) |
103 | | pub gap_ratio: f64, |
104 | | /// Coefficient of variation (measurement stability) |
105 | | pub cv: f64, |
106 | | /// GPU device name |
107 | | pub gpu_device: String, |
108 | | /// VRAM usage (MB) |
109 | | pub vram_mb: u64, |
110 | | /// Realizar latency p50 (ms) |
111 | | pub realizar_p50_ms: f64, |
112 | | /// Ollama latency p50 (ms) |
113 | | pub ollama_p50_ms: f64, |
114 | | } |
115 | | |
116 | | impl GpuParityResult { |
117 | | /// Create a new GPU parity result |
118 | | #[must_use] |
119 | 8 | pub fn new( |
120 | 8 | realizar_gpu_tps: f64, |
121 | 8 | ollama_tps: f64, |
122 | 8 | cv: f64, |
123 | 8 | gpu_device: impl Into<String>, |
124 | 8 | vram_mb: u64, |
125 | 8 | ) -> Self { |
126 | 8 | let gap_ratio = if realizar_gpu_tps > 0.0 { |
127 | 8 | ollama_tps / realizar_gpu_tps |
128 | | } else { |
129 | 0 | f64::INFINITY |
130 | | }; |
131 | | |
132 | 8 | Self { |
133 | 8 | realizar_gpu_tps, |
134 | 8 | ollama_tps, |
135 | 8 | gap_ratio, |
136 | 8 | cv, |
137 | 8 | gpu_device: gpu_device.into(), |
138 | 8 | vram_mb, |
139 | 8 | realizar_p50_ms: 0.0, |
140 | 8 | ollama_p50_ms: 0.0, |
141 | 8 | } |
142 | 8 | } |
143 | | |
144 | | /// Returns true if within 2x of Ollama (M2 target) |
145 | | #[must_use] |
146 | 3 | pub fn achieves_m2_parity(&self) -> bool { |
147 | 3 | self.gap_ratio <= 2.0 |
148 | 3 | } |
149 | | |
150 | | /// Returns true if within 1.25x of Ollama (M4 target) |
151 | | #[must_use] |
152 | 3 | pub fn achieves_m4_parity(&self) -> bool { |
153 | 3 | self.gap_ratio <= 1.25 |
154 | 3 | } |
155 | | |
156 | | /// Returns true if GPU is faster than CPU SIMD baseline (5 tok/s) |
157 | | #[must_use] |
158 | 2 | pub fn gpu_faster_than_cpu(&self) -> bool { |
159 | 2 | self.realizar_gpu_tps > 5.0 |
160 | 2 | } |
161 | | |
162 | | /// Returns true if measurements are stable (CV < 0.05) |
163 | | #[must_use] |
164 | 2 | pub fn measurements_stable(&self) -> bool { |
165 | 2 | self.cv < 0.05 |
166 | 2 | } |
167 | | |
168 | | /// Get speedup over CPU SIMD baseline |
169 | | #[must_use] |
170 | 1 | pub fn cpu_speedup(&self) -> f64 { |
171 | 1 | self.realizar_gpu_tps / 5.0 // CPU baseline ~5 tok/s |
172 | 1 | } |
173 | | } |
174 | | |
175 | | /// Gap analysis with falsifiable claims (IMP-800c) |
176 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
177 | | pub struct GapAnalysis { |
178 | | /// Claimed gap reduction |
179 | | pub claimed_gap: f64, |
180 | | /// Measured gap |
181 | | pub measured_gap: f64, |
182 | | /// Statistical significance (p-value) |
183 | | pub p_value: f64, |
184 | | /// Confidence interval lower bound (95%) |
185 | | pub ci_95_lower: f64, |
186 | | /// Confidence interval upper bound (95%) |
187 | | pub ci_95_upper: f64, |
188 | | /// Popper score (falsifiability, 0-100) |
189 | | pub popper_score: f64, |
190 | | /// Claim descriptions |
191 | | pub claims: Vec<FalsifiableClaim>, |
192 | | } |
193 | | |
194 | | /// A falsifiable claim for Popperian testing (IMP-800c) |
195 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
196 | | pub struct FalsifiableClaim { |
197 | | /// Claim identifier |
198 | | pub id: String, |
199 | | /// Claim description |
200 | | pub description: String, |
201 | | /// Expected value |
202 | | pub expected: f64, |
203 | | /// Threshold for verification |
204 | | pub threshold: f64, |
205 | | /// Measured value |
206 | | pub measured: f64, |
207 | | /// Whether claim is verified |
208 | | pub verified: bool, |
209 | | } |
210 | | |
211 | | impl FalsifiableClaim { |
212 | | /// Create a new falsifiable claim |
213 | | #[must_use] |
214 | 6 | pub fn new( |
215 | 6 | id: impl Into<String>, |
216 | 6 | description: impl Into<String>, |
217 | 6 | expected: f64, |
218 | 6 | threshold: f64, |
219 | 6 | ) -> Self { |
220 | 6 | Self { |
221 | 6 | id: id.into(), |
222 | 6 | description: description.into(), |
223 | 6 | expected, |
224 | 6 | threshold, |
225 | 6 | measured: 0.0, |
226 | 6 | verified: false, |
227 | 6 | } |
228 | 6 | } |
229 | | |
230 | | /// Evaluate the claim against a measured value |
231 | | #[must_use] |
232 | 6 | pub fn evaluate(mut self, measured: f64) -> Self { |
233 | 6 | self.measured = measured; |
234 | 6 | self.verified = measured >= self.threshold; |
235 | 6 | self |
236 | 6 | } |
237 | | } |
238 | | |
239 | | impl GapAnalysis { |
240 | | /// Create a new gap analysis |
241 | | #[must_use] |
242 | 5 | pub fn new(claimed_gap: f64, measured_gap: f64) -> Self { |
243 | 5 | Self { |
244 | 5 | claimed_gap, |
245 | 5 | measured_gap, |
246 | 5 | p_value: 0.0, |
247 | 5 | ci_95_lower: 0.0, |
248 | 5 | ci_95_upper: 0.0, |
249 | 5 | popper_score: 0.0, |
250 | 5 | claims: Vec::new(), |
251 | 5 | } |
252 | 5 | } |
253 | | |
254 | | /// Add statistical bounds |
255 | | #[must_use] |
256 | 4 | pub fn with_statistics(mut self, p_value: f64, ci_lower: f64, ci_upper: f64) -> Self { |
257 | 4 | self.p_value = p_value; |
258 | 4 | self.ci_95_lower = ci_lower; |
259 | 4 | self.ci_95_upper = ci_upper; |
260 | 4 | self |
261 | 4 | } |
262 | | |
263 | | /// Calculate and set Popper score based on claims |
264 | 1 | pub fn calculate_popper_score(&mut self) { |
265 | 1 | if self.claims.is_empty() { |
266 | 0 | self.popper_score = 0.0; |
267 | 0 | return; |
268 | 1 | } |
269 | | |
270 | 1 | let verified_count = self.claims.iter().filter(|c| c.verified).count(); |
271 | 1 | self.popper_score = (verified_count as f64 / self.claims.len() as f64) * 100.0; |
272 | 1 | } |
273 | | |
274 | | /// Add a falsifiable claim |
275 | 0 | pub fn add_claim(&mut self, claim: FalsifiableClaim) { |
276 | 0 | self.claims.push(claim); |
277 | 0 | } |
278 | | |
279 | | /// Claim is verified if measured within CI |
280 | | #[must_use] |
281 | 2 | pub fn claim_verified(&self) -> bool { |
282 | 2 | self.measured_gap >= self.ci_95_lower && self.measured_gap <= self.ci_95_upper1 |
283 | 2 | } |
284 | | |
285 | | /// Create default IMP-800c claims |
286 | | #[must_use] |
287 | 1 | pub fn with_default_claims(mut self, realizar_gpu_tps: f64) -> Self { |
288 | | // IMP-800c-1: GPU faster than CPU SIMD (>5x, threshold 25 tok/s) |
289 | 1 | self.claims.push( |
290 | 1 | FalsifiableClaim::new("IMP-800c-1", "GPU faster than CPU SIMD (>5x)", 5.0, 25.0) |
291 | 1 | .evaluate(realizar_gpu_tps), |
292 | | ); |
293 | | |
294 | | // IMP-800c-2: GPU within 10x of Ollama (threshold 24 tok/s) |
295 | 1 | self.claims.push( |
296 | 1 | FalsifiableClaim::new("IMP-800c-2", "GPU within 10x of Ollama", 10.0, 24.0) |
297 | 1 | .evaluate(realizar_gpu_tps), |
298 | | ); |
299 | | |
300 | | // IMP-800c-3: GPU within 2x of Ollama - M2 (threshold 120 tok/s) |
301 | 1 | self.claims.push( |
302 | 1 | FalsifiableClaim::new("IMP-800c-3", "GPU within 2x of Ollama (M2)", 2.0, 120.0) |
303 | 1 | .evaluate(realizar_gpu_tps), |
304 | | ); |
305 | | |
306 | | // IMP-800c-4: GPU at parity with Ollama - M4 (threshold 192 tok/s) |
307 | 1 | self.claims.push( |
308 | 1 | FalsifiableClaim::new("IMP-800c-4", "GPU at parity with Ollama (M4)", 1.25, 192.0) |
309 | 1 | .evaluate(realizar_gpu_tps), |
310 | | ); |
311 | | |
312 | 1 | self.calculate_popper_score(); |
313 | 1 | self |
314 | 1 | } |
315 | | } |
316 | | |
317 | | // ============================================================================ |
318 | | // IMP-900: Closing the 18x Gap (M3/M4 Milestones) |
319 | | // ============================================================================ |
320 | | |
321 | | /// Optimized GEMM configuration (IMP-900a) |
322 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
323 | | pub struct OptimizedGemmConfig { |
324 | | /// Tile size for shared memory (typically 32 or 64) |
325 | | pub tile_size: u32, |
326 | | /// Register blocking factor (typically 4 or 8) |
327 | | pub reg_block: u32, |
328 | | /// Use tensor cores if available (SM 7.0+) |
329 | | pub use_tensor_cores: bool, |
330 | | /// Vectorized loads (float4 = 4) |
331 | | pub vector_width: u32, |
332 | | /// Unroll factor for K-loop |
333 | | pub k_unroll: u32, |
334 | | /// Use double buffering for tile prefetch |
335 | | pub double_buffer: bool, |
336 | | } |
337 | | |
338 | | impl Default for OptimizedGemmConfig { |
339 | 6 | fn default() -> Self { |
340 | 6 | Self { |
341 | 6 | tile_size: 32, |
342 | 6 | reg_block: 4, |
343 | 6 | use_tensor_cores: false, |
344 | 6 | vector_width: 4, |
345 | 6 | k_unroll: 4, |
346 | 6 | double_buffer: true, |
347 | 6 | } |
348 | 6 | } |
349 | | } |
350 | | |
351 | | impl OptimizedGemmConfig { |
352 | | /// Create configuration for small matrices (256x256) |
353 | | #[must_use] |
354 | 0 | pub fn small() -> Self { |
355 | 0 | Self { |
356 | 0 | tile_size: 16, |
357 | 0 | reg_block: 2, |
358 | 0 | use_tensor_cores: false, |
359 | 0 | vector_width: 4, |
360 | 0 | k_unroll: 4, |
361 | 0 | double_buffer: false, |
362 | 0 | } |
363 | 0 | } |
364 | | |
365 | | /// Create configuration for large matrices (1024+) |
366 | | #[must_use] |
367 | 2 | pub fn large() -> Self { |
368 | 2 | Self { |
369 | 2 | tile_size: 64, |
370 | 2 | reg_block: 8, |
371 | 2 | use_tensor_cores: false, |
372 | 2 | vector_width: 4, |
373 | 2 | k_unroll: 8, |
374 | 2 | double_buffer: true, |
375 | 2 | } |
376 | 2 | } |
377 | | |
378 | | /// Calculate shared memory requirement (bytes) |
379 | | #[must_use] |
380 | 2 | pub fn shared_memory_bytes(&self) -> u32 { |
381 | | // Two tiles (A and B) in shared memory |
382 | | // Each tile is tile_size × tile_size × sizeof(f32) |
383 | 2 | let tile_bytes = self.tile_size * self.tile_size * 4; |
384 | 2 | if self.double_buffer { |
385 | 1 | tile_bytes * 4 // 2 tiles × 2 buffers |
386 | | } else { |
387 | 1 | tile_bytes * 2 // 2 tiles |
388 | | } |
389 | 2 | } |
390 | | |
391 | | /// Calculate threads per block |
392 | | #[must_use] |
393 | 2 | pub fn threads_per_block(&self) -> u32 { |
394 | | // Each thread computes reg_block × reg_block elements |
395 | 2 | let threads_per_dim = self.tile_size / self.reg_block; |
396 | 2 | threads_per_dim * threads_per_dim |
397 | 2 | } |
398 | | |
399 | | /// Calculate registers per thread (for accumulators) |
400 | | #[must_use] |
401 | 2 | pub fn registers_per_thread(&self) -> u32 { |
402 | | // reg_block × reg_block accumulator values |
403 | 2 | self.reg_block * self.reg_block |
404 | 2 | } |
405 | | } |
406 | | |
407 | | /// GEMM performance result (IMP-900a) |
408 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
409 | | pub struct GemmPerformanceResult { |
410 | | /// Matrix M dimension (rows of A, rows of C) |
411 | | pub m: u32, |
412 | | /// Matrix N dimension (cols of B, cols of C) |
413 | | pub n: u32, |
414 | | /// Matrix K dimension (cols of A, rows of B) |
415 | | pub k: u32, |
416 | | /// Time in milliseconds |
417 | | pub time_ms: f64, |
418 | | /// GFLOP/s achieved |
419 | | pub gflops: f64, |
420 | | /// Memory bandwidth achieved (GB/s) |
421 | | pub bandwidth_gbs: f64, |
422 | | /// Percentage of peak performance |
423 | | pub efficiency: f64, |
424 | | } |
425 | | |
426 | | impl GemmPerformanceResult { |
427 | | /// Create a new GEMM performance result |
428 | | #[must_use] |
429 | 2 | pub fn new(m: u32, n: u32, k: u32, time_ms: f64) -> Self { |
430 | | // GEMM operations: 2 * M * N * K (multiply-add) |
431 | 2 | let ops = 2.0 * f64::from(m) * f64::from(n) * f64::from(k); |
432 | 2 | let gflops = ops / (time_ms * 1e6); |
433 | | |
434 | | // Memory: read A (M*K), read B (K*N), write C (M*N) |
435 | 2 | let bytes = (f64::from(m) * f64::from(k) |
436 | 2 | + f64::from(k) * f64::from(n) |
437 | 2 | + f64::from(m) * f64::from(n)) |
438 | 2 | * 4.0; |
439 | 2 | let bandwidth_gbs = bytes / (time_ms * 1e6); |
440 | | |
441 | 2 | Self { |
442 | 2 | m, |
443 | 2 | n, |
444 | 2 | k, |
445 | 2 | time_ms, |
446 | 2 | gflops, |
447 | 2 | bandwidth_gbs, |
448 | 2 | efficiency: 0.0, // Set by caller based on peak |
449 | 2 | } |
450 | 2 | } |
451 | | |
452 | | /// Set efficiency based on peak GFLOP/s |
453 | | #[must_use] |
454 | 1 | pub fn with_peak(mut self, peak_gflops: f64) -> Self { |
455 | 1 | self.efficiency = (self.gflops / peak_gflops) * 100.0; |
456 | 1 | self |
457 | 1 | } |
458 | | |
459 | | /// Check if performance improved by at least the given factor |
460 | | #[must_use] |
461 | 2 | pub fn improved_by(&self, baseline_gflops: f64, factor: f64) -> bool { |
462 | 2 | self.gflops >= baseline_gflops * factor |
463 | 2 | } |
464 | | } |
465 | | |
466 | | /// Optimized GEMM benchmark runner (IMP-900a) |
467 | | #[derive(Debug)] |
468 | | pub struct OptimizedGemmBenchmark { |
469 | | /// Configuration |
470 | | pub config: OptimizedGemmConfig, |
471 | | /// Warmup iterations |
472 | | pub warmup_iterations: usize, |
473 | | /// Measurement iterations |
474 | | pub measurement_iterations: usize, |
475 | | /// Target coefficient of variation |
476 | | pub target_cv: f64, |
477 | | } |
478 | | |
479 | | impl Default for OptimizedGemmBenchmark { |
480 | 1 | fn default() -> Self { |
481 | 1 | Self { |
482 | 1 | config: OptimizedGemmConfig::default(), |
483 | 1 | warmup_iterations: 5, |
484 | 1 | measurement_iterations: 20, |
485 | 1 | target_cv: 0.05, |
486 | 1 | } |
487 | 1 | } |
488 | | } |
489 | | |
490 | | impl OptimizedGemmBenchmark { |
491 | | /// Create benchmark with custom config |
492 | | #[must_use] |
493 | 0 | pub fn with_config(config: OptimizedGemmConfig) -> Self { |
494 | 0 | Self { |
495 | 0 | config, |
496 | 0 | ..Default::default() |
497 | 0 | } |
498 | 0 | } |
499 | | |
500 | | /// Calculate expected improvement over naive GEMM |
501 | | #[must_use] |
502 | 1 | pub fn expected_improvement(&self) -> f64 { |
503 | 1 | let mut improvement = 1.0; |
504 | | |
505 | | // Shared memory tiling: ~2x for cache efficiency |
506 | 1 | improvement *= 2.0; |
507 | | |
508 | | // Register blocking: ~1.5x for reduced memory traffic |
509 | 1 | if self.config.reg_block >= 4 { |
510 | 1 | improvement *= 1.5; |
511 | 1 | }0 |
512 | | |
513 | | // Vectorized loads: ~1.3x for coalesced access |
514 | 1 | if self.config.vector_width >= 4 { |
515 | 1 | improvement *= 1.3; |
516 | 1 | }0 |
517 | | |
518 | | // Double buffering: ~1.2x for latency hiding |
519 | 1 | if self.config.double_buffer { |
520 | 1 | improvement *= 1.2; |
521 | 1 | }0 |
522 | | |
523 | 1 | improvement |
524 | 1 | } |
525 | | } |
526 | | |
527 | | /// Kernel fusion configuration (IMP-900b) |
528 | | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] |
529 | | pub enum FusedOpType { |
530 | | /// GEMM + bias + activation |
531 | | GemmBiasActivation, |
532 | | /// Layer normalization + linear projection |
533 | | LayerNormLinear, |
534 | | /// Fused attention (FlashAttention-style) |
535 | | FusedAttention, |
536 | | /// FFN: up projection + gate + down projection |
537 | | FusedFfn, |
538 | | } |
539 | | |
540 | | /// Fused operation specification (IMP-900b) |
541 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
542 | | pub struct FusedOpSpec { |
543 | | /// Type of fused operation |
544 | | pub op_type: FusedOpType, |
545 | | /// Input dimensions |
546 | | pub input_dims: Vec<u32>, |
547 | | /// Output dimensions |
548 | | pub output_dims: Vec<u32>, |
549 | | /// Activation function (if applicable) |
550 | | pub activation: Option<String>, |
551 | | /// Number of kernel launches when fused |
552 | | pub fused_launches: u32, |
553 | | /// Number of kernel launches when unfused |
554 | | pub unfused_launches: u32, |
555 | | } |
556 | | |
557 | | impl FusedOpSpec { |
558 | | /// Calculate launch reduction factor |
559 | | #[must_use] |
560 | 5 | pub fn launch_reduction(&self) -> f64 { |
561 | 5 | f64::from(self.unfused_launches) / f64::from(self.fused_launches) |
562 | 5 | } |
563 | | |
564 | | /// Check if fusion reduces launches by at least 50% |
565 | | #[must_use] |
566 | 4 | pub fn achieves_target_reduction(&self) -> bool { |
567 | 4 | self.launch_reduction() >= 2.0 |
568 | 4 | } |
569 | | } |
570 | | |
571 | | /// FlashAttention configuration (IMP-900c) |
572 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
573 | | pub struct FlashAttentionConfig { |
574 | | /// Block size for Q tiling (Br) |
575 | | pub block_size_q: u32, |
576 | | /// Block size for K/V tiling (Bc) |
577 | | pub block_size_kv: u32, |
578 | | /// Head dimension |
579 | | pub head_dim: u32, |
580 | | /// Number of attention heads |
581 | | pub num_heads: u32, |
582 | | /// Use causal masking |
583 | | pub causal: bool, |
584 | | /// Softmax scale (default: 1/sqrt(head_dim)) |
585 | | pub scale: f32, |
586 | | } |
587 | | |
588 | | impl FlashAttentionConfig { |
589 | | /// Create configuration for phi-2 model |
590 | | #[must_use] |
591 | 3 | pub fn phi2() -> Self { |
592 | 3 | Self { |
593 | 3 | block_size_q: 64, |
594 | 3 | block_size_kv: 64, |
595 | 3 | head_dim: 80, // phi-2: 2560 / 32 heads |
596 | 3 | num_heads: 32, |
597 | 3 | causal: true, |
598 | 3 | scale: 1.0 / (80.0_f32).sqrt(), |
599 | 3 | } |
600 | 3 | } |
601 | | |
602 | | /// Calculate memory required for attention (naive vs flash) |
603 | | #[must_use] |
604 | 4 | pub fn memory_comparison(&self, seq_len: u32) -> (u64, u64) { |
605 | | // Naive: O(N²) attention matrix |
606 | 4 | let naive_bytes = u64::from(seq_len) * u64::from(seq_len) * 4; |
607 | | |
608 | | // FlashAttention: O(N) working memory |
609 | 4 | let flash_bytes = u64::from(self.block_size_q) * u64::from(self.block_size_kv) * 4 * 2; // S and P blocks |
610 | | |
611 | 4 | (naive_bytes, flash_bytes) |
612 | 4 | } |
613 | | |
614 | | /// Calculate memory savings factor |
615 | | #[must_use] |
616 | 2 | pub fn memory_savings(&self, seq_len: u32) -> f64 { |
617 | 2 | let (naive, flash) = self.memory_comparison(seq_len); |
618 | 2 | naive as f64 / flash as f64 |
619 | 2 | } |
620 | | } |
621 | | |
622 | | /// Memory pool configuration (IMP-900d) |
623 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
624 | | pub struct MemoryPoolConfig { |
625 | | /// Initial pool size (bytes) |
626 | | pub initial_size: usize, |
627 | | /// Maximum pool size (bytes) |
628 | | pub max_size: usize, |
629 | | /// Size classes for allocation (powers of 2) |
630 | | pub size_classes: Vec<usize>, |
631 | | /// Use pinned memory for host staging |
632 | | pub use_pinned_memory: bool, |
633 | | /// Enable async transfers |
634 | | pub async_transfers: bool, |
635 | | } |
636 | | |
637 | | impl Default for MemoryPoolConfig { |
638 | 4 | fn default() -> Self { |
639 | 4 | Self { |
640 | 4 | initial_size: 256 * 1024 * 1024, // 256 MB |
641 | 4 | max_size: 2 * 1024 * 1024 * 1024, // 2 GB |
642 | 4 | size_classes: vec![ |
643 | 4 | 4096, // 4 KB |
644 | 4 | 16384, // 16 KB |
645 | 4 | 65536, // 64 KB |
646 | 4 | 262_144, // 256 KB |
647 | 4 | 1_048_576, // 1 MB |
648 | 4 | 4_194_304, // 4 MB |
649 | 4 | 16_777_216, // 16 MB |
650 | 4 | 67_108_864, // 64 MB |
651 | 4 | 268_435_456, // 256 MB |
652 | 4 | ], |
653 | 4 | use_pinned_memory: true, |
654 | 4 | async_transfers: true, |
655 | 4 | } |
656 | 4 | } |
657 | | } |
658 | | |
659 | | impl MemoryPoolConfig { |
660 | | /// Find the smallest size class that fits the requested size |
661 | | #[must_use] |
662 | 4 | pub fn find_size_class(&self, requested: usize) -> Option<usize> { |
663 | 4 | self.size_classes |
664 | 4 | .iter() |
665 | 4 | .copied() |
666 | 24 | .find4 (|&size| size >= requested) |
667 | 4 | } |
668 | | |
669 | | /// Calculate expected bandwidth improvement from pinned memory |
670 | | #[must_use] |
671 | 2 | pub fn expected_bandwidth_improvement(&self) -> f64 { |
672 | 2 | if self.use_pinned_memory { |
673 | 1 | 2.4 // Pinned memory typically 2-3x faster |
674 | | } else { |
675 | 1 | 1.0 |
676 | | } |
677 | 2 | } |
678 | | } |
679 | | |
680 | | /// IMP-900 combined result (M3/M4 targets) |
681 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
682 | | pub struct Imp900Result { |
683 | | /// Baseline throughput (13.1 tok/s from IMP-800) |
684 | | pub baseline_tps: f64, |
685 | | /// Throughput after optimizations |
686 | | pub optimized_tps: f64, |
687 | | /// GEMM optimization improvement factor |
688 | | pub gemm_improvement: f64, |
689 | | /// Kernel fusion improvement factor |
690 | | pub fusion_improvement: f64, |
691 | | /// FlashAttention improvement factor |
692 | | pub flash_attention_improvement: f64, |
693 | | /// Memory optimization improvement factor |
694 | | pub memory_improvement: f64, |
695 | | /// Gap to Ollama |
696 | | pub gap_ratio: f64, |
697 | | /// Target milestone achieved |
698 | | pub milestone: Option<String>, |
699 | | } |
700 | | |
701 | | impl Imp900Result { |
702 | | /// Create result from baseline |
703 | | #[must_use] |
704 | 5 | pub fn from_baseline(baseline_tps: f64) -> Self { |
705 | 5 | Self { |
706 | 5 | baseline_tps, |
707 | 5 | optimized_tps: baseline_tps, |
708 | 5 | gemm_improvement: 1.0, |
709 | 5 | fusion_improvement: 1.0, |
710 | 5 | flash_attention_improvement: 1.0, |
711 | 5 | memory_improvement: 1.0, |
712 | 5 | gap_ratio: 240.0 / baseline_tps, |
713 | 5 | milestone: None, |
714 | 5 | } |
715 | 5 | } |
716 | | |
717 | | /// Apply GEMM optimization |
718 | | #[must_use] |
719 | 4 | pub fn with_gemm_improvement(mut self, factor: f64) -> Self { |
720 | 4 | self.gemm_improvement = factor; |
721 | 4 | self.recalculate(); |
722 | 4 | self |
723 | 4 | } |
724 | | |
725 | | /// Apply fusion optimization |
726 | | #[must_use] |
727 | 2 | pub fn with_fusion_improvement(mut self, factor: f64) -> Self { |
728 | 2 | self.fusion_improvement = factor; |
729 | 2 | self.recalculate(); |
730 | 2 | self |
731 | 2 | } |
732 | | |
733 | | /// Apply FlashAttention optimization |
734 | | #[must_use] |
735 | 2 | pub fn with_flash_attention_improvement(mut self, factor: f64) -> Self { |
736 | 2 | self.flash_attention_improvement = factor; |
737 | 2 | self.recalculate(); |
738 | 2 | self |
739 | 2 | } |
740 | | |
741 | | /// Apply memory optimization |
742 | | #[must_use] |
743 | 3 | pub fn with_memory_improvement(mut self, factor: f64) -> Self { |
744 | 3 | self.memory_improvement = factor; |
745 | 3 | self.recalculate(); |
746 | 3 | self |
747 | 3 | } |
748 | | |
749 | | /// Recalculate throughput and milestone |
750 | 11 | fn recalculate(&mut self) { |
751 | 11 | let total_improvement = self.gemm_improvement |
752 | 11 | * self.fusion_improvement |
753 | 11 | * self.flash_attention_improvement |
754 | 11 | * self.memory_improvement; |
755 | | |
756 | 11 | self.optimized_tps = self.baseline_tps * total_improvement; |
757 | 11 | self.gap_ratio = 240.0 / self.optimized_tps; |
758 | | |
759 | 11 | self.milestone = if self.gap_ratio <= 1.25 { |
760 | 2 | Some("M4".to_string()) // Full parity |
761 | 9 | } else if self.gap_ratio <= 2.0 { |
762 | 0 | Some("M3".to_string()) // Near parity |
763 | 9 | } else if self.gap_ratio <= 5.0 { |
764 | 4 | Some("M2".to_string()) // Within 5x |
765 | | } else { |
766 | 5 | None |
767 | | }; |
768 | 11 | } |
769 | | |
770 | | /// Check if M3 target achieved (>48 tok/s, <5x gap) |
771 | | #[must_use] |
772 | 1 | pub fn achieves_m3(&self) -> bool { |
773 | 1 | self.optimized_tps >= 48.0 && self.gap_ratio <= 5.0 |
774 | 1 | } |
775 | | |
776 | | /// Check if M4 target achieved (>192 tok/s, <1.25x gap) |
777 | | #[must_use] |
778 | 1 | pub fn achieves_m4(&self) -> bool { |
779 | 1 | self.optimized_tps >= 192.0 && self.gap_ratio <= 1.25 |
780 | 1 | } |
781 | | |
782 | | /// Get combined improvement factor |
783 | | #[must_use] |
784 | 1 | pub fn total_improvement(&self) -> f64 { |
785 | 1 | self.optimized_tps / self.baseline_tps |
786 | 1 | } |
787 | | } |