aprender-serve 0.64.0

Pure Rust ML inference engine built from scratch - model serving for GGUF and safetensors

#[test]
fn test_parity016d_batch_generate_gpu_path() {
    // Test the integration point for GPU batch forward in batch_generate()
    //
    // Current batch_generate() flow:
    // 1. Prefill: each prompt processed sequentially
    // 2. Generate: for each step, loop over active requests
    //
    // GPU-optimized flow:
    // 1. Prefill: batch all prompts together (GPU GEMM)
    // 2. Generate: batch all active requests together (GPU GEMM when >= 32)

    let batch_sizes = [1, 8, 16, 32, 64];

    println!("\nPARITY-016d: Batch Generate GPU Path Design");
    println!("  Batch Size | GPU Path | Expected Speedup");
    println!("  -----------|----------|------------------");

    for &batch in &batch_sizes {
        let use_gpu = batch >= 32;
        let expected_speedup = if use_gpu { "~10x" } else { "1x (CPU)" };
        println!("  {:10} | {:8} | {}", batch, use_gpu, expected_speedup);
    }

    // Key integration points:
    // 1. In batch_generate(), check active_count >= 32
    // 2. If true, collect all active hidden states into batch tensor
    // 3. Call gpu_batch_ffn() instead of per-request forward
    // 4. Distribute results back to individual requests

    struct BatchGenerateGPUConfig {
        gpu_threshold: usize,
        prefetch_dequant: bool,
        async_gpu_transfer: bool,
    }

    let config = BatchGenerateGPUConfig {
        gpu_threshold: 32,
        prefetch_dequant: true,
        async_gpu_transfer: false,
    };

    println!("\n  Configuration:");
    println!(
        "    GPU threshold: {} active requests",
        config.gpu_threshold
    );
    println!("    Prefetch dequant: {}", config.prefetch_dequant);
    println!("    Async transfer: {}", config.async_gpu_transfer);

    assert!(
        config.gpu_threshold >= 32,
        "PARITY-016d: GPU threshold should be >= 32 for GEMM benefit"
    );

    println!("  Status: VERIFIED - Integration design complete");
}

#[test]
fn test_parity016e_performance_projection() {
    // Calculate expected throughput with GPU batch FFN
    //
    // Current performance (single request):
    // - KV cache: 5.09 tok/s
    // - Gap to Ollama (225 tok/s): 44x
    //
    // With GPU batch FFN at batch=64:
    // - FFN speedup: ~10x (from GEMM vs MATVEC)
    // - Total speedup: ~3-5x (FFN is ~30% of forward pass)
    // - Expected per-request: ~15-25 tok/s
    // - Expected total throughput: ~1000-1600 tok/s

    let current_single_tps = 5.09;
    let ollama_tps = 225.0;
    let current_gap = ollama_tps / current_single_tps;

    println!("\nPARITY-016e: Performance Projection");
    println!("\n  Current State:");
    println!("    Single request: {:.2} tok/s", current_single_tps);
    println!("    Ollama baseline: {:.0} tok/s", ollama_tps);
    println!("    Gap: {:.1}x", current_gap);

    // FFN is ~30% of forward pass time
    let ffn_fraction = 0.30;
    let ffn_speedup = 10.0; // From GEMM vs MATVEC

    // Calculate new forward time
    // new_time = (1 - ffn_fraction) * old_time + (ffn_fraction / ffn_speedup) * old_time
    // new_time = old_time * ((1 - ffn_fraction) + ffn_fraction / ffn_speedup)
    // new_time = old_time * (0.7 + 0.03) = old_time * 0.73
    let time_multiplier = (1.0 - ffn_fraction) + (ffn_fraction / ffn_speedup);
    let per_request_speedup = 1.0 / time_multiplier;
    let expected_per_request_tps = current_single_tps * per_request_speedup;

    println!("\n  With GPU Batch FFN (batch=64):");
    println!("    FFN fraction of forward: {:.0}%", ffn_fraction * 100.0);
    println!("    FFN speedup from GPU: {:.0}x", ffn_speedup);
    println!("    Time multiplier: {:.2}x", time_multiplier);
    println!("    Per-request speedup: {:.2}x", per_request_speedup);
    println!(
        "    Expected per-request: {:.1} tok/s",
        expected_per_request_tps
    );

    // Total throughput for batch
    let batch_size = 64.0;
    let expected_total_tps = expected_per_request_tps * batch_size;
    let new_gap = ollama_tps / expected_per_request_tps;

    println!("\n  Batch Throughput (batch=64):");
    println!("    Total throughput: {:.0} tok/s", expected_total_tps);
    println!("    Gap to Ollama (per-request): {:.1}x", new_gap);

    // Verify projections are reasonable
    assert!(
        per_request_speedup > 1.0 && per_request_speedup < 10.0,
        "PARITY-016e: Per-request speedup should be reasonable (1-10x)"
    );
    assert!(
        expected_total_tps > 100.0,
        "PARITY-016e: Total throughput should be > 100 tok/s"
    );

    // Summary
    println!("\n  Summary:");
    println!(
        "    ✅ GPU batch FFN reduces gap from {:.0}x to {:.1}x (per-request)",
        current_gap, new_gap
    );
    println!(
        "    ✅ Total throughput: {:.0} tok/s at batch=64",
        expected_total_tps
    );
    println!("    ⚠️  For full parity, need: FlashAttention + quantized GEMM");

    println!("  Status: VERIFIED - Performance projection complete");
}

// ============================================================================

// PARITY-017: Actual batch_generate GPU Path Implementation
// ============================================================================
//
// Objective: Actually implement GPU batch forward in batch_generate()
//
// From PARITY-016:
// - GPU batch matmul: 8.56 GFLOPS
// - HybridScheduler dispatches GPU for batch >= 32
// - Projected: 446 tok/s total at batch=64
//
// Implementation:
// 1. gpu_batch_ffn(): Batch FFN through HybridScheduler
// 2. forward_batch_with_gpu(): Single forward pass for batch of tokens
// 3. batch_generate_gpu(): Modified batch_generate using GPU path
// ============================================================================

#[test]
fn test_parity017a_gpu_batch_ffn_implementation() {
    use crate::gpu::HybridScheduler;

    // Implement the actual gpu_batch_ffn function
    // This processes [batch, hidden] -> [batch, hidden] through FFN with GPU

    fn gpu_batch_ffn(
        input: &[f32],       // [batch, hidden] flattened
        up_weight: &[f32],   // [hidden, intermediate]
        down_weight: &[f32], // [intermediate, hidden]
        batch_size: usize,
        hidden_dim: usize,
        intermediate_dim: usize,
        scheduler: &mut HybridScheduler,
    ) -> std::result::Result<Vec<f32>, String> {
        // Step 1: Up projection [batch, hidden] @ [hidden, intermediate] = [batch, intermediate]
        let intermediate = scheduler
            .matmul(input, up_weight, batch_size, hidden_dim, intermediate_dim)
            .map_err(|e| format!("Up projection failed: {:?}", e))?;

        // Step 2: GELU activation (in-place would be better)
        let activated: Vec<f32> = intermediate
            .iter()
            .map(|&x| {
                let x64 = x as f64;
                (x64 * 0.5 * (1.0 + (x64 * 0.7978845608 * (1.0 + 0.044715 * x64 * x64)).tanh()))
                    as f32
            })
            .collect();

        // Step 3: Down projection [batch, intermediate] @ [intermediate, hidden] = [batch, hidden]
        let output = scheduler
            .matmul(
                &activated,
                down_weight,
                batch_size,
                intermediate_dim,
                hidden_dim,
            )
            .map_err(|e| format!("Down projection failed: {:?}", e))?;

        Ok(output)
    }

    // SCALED DOWN from phi-2 dimensions (batch 32, hidden 2560, intermediate 10240).
    // This test asserts a SHAPE property of the up -> GELU -> down chain, and a
    // shape property does not need production-sized tensors. At phi-2 size each
    // projection is 32*2560*10240 = 839M MACs; `HybridScheduler::new()` succeeds
    // without a GPU (`GpuCompute::auto()` falls back to CPU), so on a CPU-only
    // runner both projections ran through `cpu_matmul` and this test alone cost
    // minutes of workspace-test wall clock — times three under nextest's
    // `retries = 2`. These dimensions do the same work 1600x smaller.
    //
    // The dimensions are chosen so m*k*n stays ABOVE `HybridScheduler`'s
    // `gpu_threshold` (64*64*64 = 262_144): up and down are both
    // 8*128*512 = 524_288, so the GPU-vs-CPU dispatch decision this test
    // exercises is UNCHANGED on a GPU-equipped host.
    //
    // The throughput/GFLOPS reporting was removed rather than rescaled: at this
    // size the number is meaningless, and a printed rate invites someone to cite
    // it. Benchmarks belong in `benches/`, not in `--lib` tests.
    let batch_size = 8;
    let hidden_dim = 128;
    let intermediate_dim = 512;

    // Create test data
    let input: Vec<f32> = (0..batch_size * hidden_dim)
        .map(|i| (i as f32 * 0.001).sin() * 0.1)
        .collect();
    let up_weight: Vec<f32> = (0..hidden_dim * intermediate_dim)
        .map(|i| (i as f32 * 0.0001).cos() * 0.01)
        .collect();
    let down_weight: Vec<f32> = (0..intermediate_dim * hidden_dim)
        .map(|i| (i as f32 * 0.0001).sin() * 0.01)
        .collect();

    println!("\nPARITY-017a: GPU Batch FFN Implementation");
    println!("  Input: [{}x{}]", batch_size, hidden_dim);
    println!("  Up: [{}x{}]", hidden_dim, intermediate_dim);
    println!("  Down: [{}x{}]", intermediate_dim, hidden_dim);

    if let Ok(mut scheduler) = HybridScheduler::new() {
        let result = gpu_batch_ffn(
            &input,
            &up_weight,
            &down_weight,
            batch_size,
            hidden_dim,
            intermediate_dim,
            &mut scheduler,
        );

        match result {
            Ok(output) => {
                assert_eq!(
                    output.len(),
                    batch_size * hidden_dim,
                    "PARITY-017a: Output should be [batch, hidden]"
                );

                println!("  Output: [{}x{}]", batch_size, hidden_dim);
                println!("  Status: VERIFIED - GPU batch FFN works");
            },
            Err(e) => {
                // This arm used to `println!("SKIP - GPU path failed")` and PASS,
                // which made the assertion above unreachable for any defect that
                // surfaces as an `Err` — the test excluded no outcome on a
                // GPU-equipped host. Verified by mutation: truncating
                // `HybridScheduler::matmul`'s output by one element makes the down
                // projection return `InvalidShape`, and the test reported `ok` at
                // BOTH the scaled and the original [32x2560] dimensions. The input
                // here is small and valid, so an `Err` is a real defect, not a
                // capability gap.
                panic!("PARITY-017a: gpu_batch_ffn failed on valid input: {}", e);
            },
        }
    } else {
        println!("  Status: SKIP - GPU not available");
    }
}

#[test]
fn test_parity017b_batch_forward_with_gpu_ffn() {
    // Simulate a full forward pass with GPU-accelerated FFN
    //
    // The forward pass consists of:
    // 1. Embedding (CPU, fast table lookup)
    // 2. Layer norm (CPU, batch-parallel)
    // 3. Attention (CPU for now - MATVEC for single-token per request)
    // 4. FFN (GPU GEMM when batch >= 32) <-- This is GPU accelerated
    // 5. Output projection (CPU or GPU depending on batch)

    let batch_size = 32;
    let _hidden_dim = 2560;
    let _intermediate_dim = 10240;
    let num_layers = 32;

    // Simulate forward pass timing
    struct ForwardTiming {
        embed_us: u64,
        ln_us: u64,
        attn_us: u64,
        ffn_us: u64,
        output_us: u64,
    }

    // Baseline CPU timing (estimated from single-request)
    let cpu_timing = ForwardTiming {
        embed_us: 100,   // Fast table lookup
        ln_us: 500,      // Layer norm
        attn_us: 5000,   // Attention (MATVEC)
        ffn_us: 15000,   // FFN (MATVEC)
        output_us: 1000, // Output projection
    };

    // GPU timing (FFN as GEMM)
    let gpu_timing = ForwardTiming {
        embed_us: 100,  // Same
        ln_us: 500,     // Same
        attn_us: 5000,  // Same (still MATVEC)
        ffn_us: 1500,   // ~10x faster with GPU GEMM
        output_us: 500, // Slight improvement
    };

    let cpu_total_per_layer = cpu_timing.embed_us
        + cpu_timing.ln_us
        + cpu_timing.attn_us
        + cpu_timing.ffn_us
        + cpu_timing.output_us;
    let gpu_total_per_layer = gpu_timing.embed_us
        + gpu_timing.ln_us
        + gpu_timing.attn_us
        + gpu_timing.ffn_us
        + gpu_timing.output_us;

    let cpu_total_ms = (cpu_total_per_layer * num_layers as u64) as f64 / 1000.0;
    let gpu_total_ms = (gpu_total_per_layer * num_layers as u64) as f64 / 1000.0;
    let speedup = cpu_total_ms / gpu_total_ms;

    println!("\nPARITY-017b: Batch Forward with GPU FFN");
    println!("\n  Per-Layer Timing (microseconds):");
    println!("  Component    | CPU     | GPU     | Speedup");
    println!("  -------------|---------|---------|--------");
    println!(
        "  Embed        | {:7} | {:7} | {:.1}x",
        cpu_timing.embed_us,
        gpu_timing.embed_us,
        cpu_timing.embed_us as f64 / gpu_timing.embed_us as f64
    );
    println!(
        "  LayerNorm    | {:7} | {:7} | {:.1}x",
        cpu_timing.ln_us,
        gpu_timing.ln_us,
        cpu_timing.ln_us as f64 / gpu_timing.ln_us as f64
    );
    println!(
        "  Attention    | {:7} | {:7} | {:.1}x",
        cpu_timing.attn_us,
        gpu_timing.attn_us,
        cpu_timing.attn_us as f64 / gpu_timing.attn_us as f64
    );
    println!(
        "  FFN          | {:7} | {:7} | {:.1}x",
        cpu_timing.ffn_us,
        gpu_timing.ffn_us,
        cpu_timing.ffn_us as f64 / gpu_timing.ffn_us as f64
    );
    println!(
        "  Output       | {:7} | {:7} | {:.1}x",
        cpu_timing.output_us,
        gpu_timing.output_us,
        cpu_timing.output_us as f64 / gpu_timing.output_us as f64
    );

    println!("\n  Total ({} layers):", num_layers);
    println!("    CPU: {:.1}ms", cpu_total_ms);
    println!("    GPU: {:.1}ms", gpu_total_ms);
    println!("    Speedup: {:.2}x", speedup);

    let tokens_per_step = batch_size;
    let cpu_tps = tokens_per_step as f64 / (cpu_total_ms / 1000.0);
    let gpu_tps = tokens_per_step as f64 / (gpu_total_ms / 1000.0);

    println!("\n  Throughput (batch={}):", batch_size);
    println!("    CPU: {:.0} tok/s", cpu_tps);
    println!("    GPU: {:.0} tok/s", gpu_tps);

    assert!(speedup > 1.0, "PARITY-017b: GPU should be faster");
    assert!(
        gpu_tps > 100.0,
        "PARITY-017b: GPU throughput should be > 100 tok/s"
    );

    println!(
        "  Status: VERIFIED - GPU FFN provides {:.2}x speedup",
        speedup
    );
}