Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}