SPB Git

spb/metrika Public

Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.

Swift 92.4% HTML 3.3% R 3% Shell 1.3%
4.2 KB · 120 lines swift
Raw Blame History
1//2//  GPUBootstrapTests.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Foundation11import Testing12import ZQGPU13import ZQParser14import ZQPlanner15import ZQStats1617/// GPU bootstrap cross-checks (CLAUDE.md §9): same seed ⇒ identical18/// resample indices across backends; estimates within the documented19/// float32 tolerance.20@Suite("GPU bootstrap", .enabled(if: ZQGPUBootstrap.isAvailable))21struct GPUBootstrapTests {2223    @Test("MLX Philox indices are bit-identical to the CPU reference")24    func philoxParity() {25        let seed: UInt64 = 4226        let n = 13727        let generator = Philox4x32(seed: seed)2829        for replicate in [0, 1, 7, 1000] {30            let cpu = ZQResampling.pairsBootstrapIndices(31                replicate: replicate, sampleSize: n, generator: generator32            )33            let gpu = ZQGPUBootstrap.gpuIndices(replicate: replicate, sampleSize: n, seed: seed)34            #expect(cpu == gpu, "replicate \(replicate)")35        }36    }3738    @Test("GPU coefficient draws match CPU within float32 tolerance")39    func drawParity() throws {40        // Small synthetic problem, well conditioned.41        let n = 20042        let generator = Philox4x32(seed: 7)43        let x = (0..<n).map { i in 5 + 10 * generator.uniform(at: UInt64(i)) }44        let y = (0..<n).map { i in45            2 + 0.5 * x[i] + (generator.uniform(at: UInt64(n + i)) - 0.5)46        }47        let reps = 504849        let gpuDraws = ZQGPUBootstrap.pairsBootstrapOLS(50            y: y, predictors: [("x", x)], replicates: reps, seed: 4251        )5253        let bootstrapGenerator = Philox4x32(seed: 42)54        for replicate in 0..<reps {55            let indices = ZQResampling.pairsBootstrapIndices(56                replicate: replicate, sampleSize: n, generator: bootstrapGenerator57            )58            let fit = try ZQOLS.fit(59                y: indices.map { y[$0] },60                predictors: [("x", indices.map { x[$0] })]61            )62            for (j, coefficient) in fit.coefficients.enumerated() {63                expectClose(64                    gpuDraws[replicate][j], coefficient.estimate,65                    rtol: 1e-4, "replicate \(replicate) b[\(coefficient.name)]"66                )67            }68        }69    }7071    @Test("GPU bootstrap is deterministic for a fixed seed")72    func determinism() {73        let n = 10074        let generator = Philox4x32(seed: 3)75        let x = (0..<n).map { i in generator.uniform(at: UInt64(i)) }76        let y = (0..<n).map { i in x[i] + generator.uniform(at: UInt64(n + i)) }7778        let first = ZQGPUBootstrap.pairsBootstrapOLS(79            y: y, predictors: [("x", x)], replicates: 20, seed: 9980        )81        let second = ZQGPUBootstrap.pairsBootstrapOLS(82            y: y, predictors: [("x", x)], replicates: 20, seed: 9983        )84        #expect(first == second)85    }8687    @Test("chunking does not change results")88    func chunkingInvariance() {89        let n = 8090        let generator = Philox4x32(seed: 5)91        let x = (0..<n).map { i in generator.uniform(at: UInt64(i)) }92        let y = (0..<n).map { i in 2 * x[i] + generator.uniform(at: UInt64(n + i)) }9394        let oneChunk = ZQGPUBootstrap.pairsBootstrapOLS(95            y: y, predictors: [("x", x)], replicates: 30, seed: 1196        )97        // Budget so small every chunk holds a single replicate.98        let manyChunks = ZQGPUBootstrap.pairsBootstrapOLS(99            y: y, predictors: [("x", x)], replicates: 30, seed: 11,100            memoryBudgetBytes: 1101        )102        #expect(oneChunk == manyChunks)103    }104105    @Test("planner sends large-reps bootstrap to the GPU")106    func plannerDispatch() throws {107        let planner = ZQPlanner(gpuAvailable: true)108        let parser = ZQCommandParser()109110        let large = try #require(try parser.parse("bootstrap, reps(10000): reg y x"))111        #expect(planner.plan(large, rowCount: 1000).backend == .gpu)112113        let small = try #require(try parser.parse("bootstrap, reps(100): reg y x"))114        #expect(planner.plan(small, rowCount: 1000).backend == .cpu)115116        let noGPU = ZQPlanner(gpuAvailable: false)117        #expect(noGPU.plan(large, rowCount: 1000).backend == .cpu)118    }119}120