|
- //go:build cufft && windows
-
- package gpudemod
-
- import (
- "os"
- "path/filepath"
- "testing"
- )
-
- func configureNativePreparedDLLPath(t *testing.T) {
- t.Helper()
- candidates := []string{
- filepath.Join("build", "gpudemod_kernels.dll"),
- filepath.Join("internal", "demod", "gpudemod", "build", "gpudemod_kernels.dll"),
- "gpudemod_kernels.dll",
- }
- for _, candidate := range candidates {
- if _, err := os.Stat(candidate); err == nil {
- abs, err := filepath.Abs(candidate)
- if err != nil {
- t.Fatalf("resolve native prepared DLL path: %v", err)
- }
- t.Setenv("GPUMOD_DLL", abs)
- return
- }
- }
- }
-
- func requireNativePreparedTestRunner(t *testing.T) *BatchRunner {
- t.Helper()
- configureNativePreparedDLLPath(t)
- if err := ensureDLLLoaded(); err != nil {
- t.Skipf("native prepared path unavailable: %v", err)
- }
- if !Available() {
- t.Skip("native prepared path unavailable: cuda device not available")
- }
- r, err := NewBatchRunner(32768, 4000000)
- if err != nil {
- t.Skipf("native prepared path unavailable: %v", err)
- }
- t.Cleanup(r.Close)
- return r
- }
-
- func TestStreamingGPUNativePreparedMatchesCPUOracleAcrossChunkPatterns(t *testing.T) {
- job := StreamingExtractJob{
- SignalID: 1,
- OffsetHz: 12500,
- Bandwidth: 20000,
- OutRate: 200000,
- NumTaps: 65,
- ConfigHash: 777,
- }
- exec := func(r *BatchRunner, invocations []StreamingGPUInvocation) ([]StreamingGPUExecutionResult, error) {
- return r.executeStreamingGPUNativePrepared(invocations)
- }
- t.Run("DeterministicIQ", func(t *testing.T) {
- r := requireNativePreparedTestRunner(t)
- steps := makeStreamingValidationSteps(
- makeDeterministicIQ(8192),
- []int{0, 1, 2, 17, 63, 64, 65, 129, 511, 2048},
- []StreamingExtractJob{job},
- )
- runPreparedSequenceAgainstOracle(t, r, exec, steps, 1e-4, 1e-8)
- })
- t.Run("ToneNoiseIQ", func(t *testing.T) {
- r := requireNativePreparedTestRunner(t)
- steps := makeStreamingValidationSteps(
- makeToneNoiseIQ(12288, 0.023),
- []int{7, 20, 3, 63, 64, 65, 777, 2048, 4096},
- []StreamingExtractJob{job},
- )
- runPreparedSequenceAgainstOracle(t, r, exec, steps, 1e-4, 1e-8)
- })
- }
-
- func TestStreamingGPUNativePreparedLifecycleResetAndCapacity(t *testing.T) {
- r := requireNativePreparedTestRunner(t)
- exec := func(invocations []StreamingGPUInvocation) ([]StreamingGPUExecutionResult, error) {
- return r.executeStreamingGPUNativePrepared(invocations)
- }
- jobA := StreamingExtractJob{
- SignalID: 11,
- OffsetHz: 12500,
- Bandwidth: 20000,
- OutRate: 200000,
- NumTaps: 65,
- ConfigHash: 3001,
- }
- jobB := StreamingExtractJob{
- SignalID: 22,
- OffsetHz: -18750,
- Bandwidth: 16000,
- OutRate: 100000,
- NumTaps: 33,
- ConfigHash: 4002,
- }
-
- steps := []streamingValidationStep{
- {
- name: "prime_both_signals",
- iq: makeDeterministicIQ(256),
- jobs: []StreamingExtractJob{jobA, jobB},
- },
- {
- name: "grow_capacity",
- iq: makeToneNoiseIQ(4096, 0.037),
- jobs: []StreamingExtractJob{jobA, jobB},
- },
- {
- name: "config_reset_zero_new",
- iq: nil,
- jobs: []StreamingExtractJob{{SignalID: jobA.SignalID, OffsetHz: jobA.OffsetHz, Bandwidth: jobA.Bandwidth, OutRate: jobA.OutRate, NumTaps: jobA.NumTaps, ConfigHash: jobA.ConfigHash + 1}, jobB},
- },
- {
- name: "signal_b_disappears",
- iq: makeDeterministicIQ(64),
- jobs: []StreamingExtractJob{jobA},
- },
- {
- name: "signal_b_reappears",
- iq: makeToneNoiseIQ(96, 0.017),
- jobs: []StreamingExtractJob{jobA, jobB},
- },
- {
- name: "history_boundary",
- iq: makeDeterministicIQ(65),
- jobs: []StreamingExtractJob{jobA, jobB},
- },
- }
-
- oracle := NewCPUOracleRunner(r.eng.sampleRate)
- var grownCap int
- for idx, step := range steps {
- invocations, err := r.buildStreamingGPUInvocations(step.iq, step.jobs)
- if err != nil {
- t.Fatalf("step %d (%s): build invocations failed: %v", idx, step.name, err)
- }
- got, err := exec(invocations)
- if err != nil {
- t.Fatalf("step %d (%s): native prepared exec failed: %v", idx, step.name, err)
- }
- want, err := oracle.StreamingExtract(step.iq, step.jobs)
- if err != nil {
- t.Fatalf("step %d (%s): oracle failed: %v", idx, step.name, err)
- }
- if len(got) != len(want) {
- t.Fatalf("step %d (%s): result count mismatch: got=%d want=%d", idx, step.name, len(got), len(want))
- }
- applied := r.applyStreamingGPUExecutionResults(got)
- for i, job := range step.jobs {
- oracleState := oracle.States[job.SignalID]
- requirePreparedExecutionResultMatchesOracle(t, got[i], want[i], oracleState, 1e-4, 1e-8)
- requireStreamingExtractResultMatchesOracle(t, applied[i], want[i])
- requireExtractStateMatchesOracle(t, r.streamState[job.SignalID], oracleState, 1e-8, 1e-4)
-
- state := r.nativeState[job.SignalID]
- if state == nil {
- t.Fatalf("step %d (%s): missing native state for signal %d", idx, step.name, job.SignalID)
- }
- if state.configHash != job.ConfigHash {
- t.Fatalf("step %d (%s): native config hash mismatch for signal %d: got=%d want=%d", idx, step.name, job.SignalID, state.configHash, job.ConfigHash)
- }
- if state.decim != oracleState.Decim {
- t.Fatalf("step %d (%s): native decim mismatch for signal %d: got=%d want=%d", idx, step.name, job.SignalID, state.decim, oracleState.Decim)
- }
- if state.numTaps != oracleState.NumTaps {
- t.Fatalf("step %d (%s): native num taps mismatch for signal %d: got=%d want=%d", idx, step.name, job.SignalID, state.numTaps, oracleState.NumTaps)
- }
- if state.historyCap != maxInt(0, oracleState.NumTaps-1) {
- t.Fatalf("step %d (%s): native history cap mismatch for signal %d: got=%d want=%d", idx, step.name, job.SignalID, state.historyCap, maxInt(0, oracleState.NumTaps-1))
- }
- if state.historyLen != len(oracleState.ShiftedHistory) {
- t.Fatalf("step %d (%s): native history len mismatch for signal %d: got=%d want=%d", idx, step.name, job.SignalID, state.historyLen, len(oracleState.ShiftedHistory))
- }
- if len(step.iq) > 0 && state.shiftedCap < len(step.iq) {
- t.Fatalf("step %d (%s): native shifted capacity too small for signal %d: got=%d need>=%d", idx, step.name, job.SignalID, state.shiftedCap, len(step.iq))
- }
- if state.outCap < got[i].NOut {
- t.Fatalf("step %d (%s): native out capacity too small for signal %d: got=%d need>=%d", idx, step.name, job.SignalID, state.outCap, got[i].NOut)
- }
- if job.SignalID == jobA.SignalID && state.shiftedCap > grownCap {
- grownCap = state.shiftedCap
- }
- }
- if step.name == "grow_capacity" && grownCap < len(step.iq) {
- t.Fatalf("expected capacity growth for signal %d, got=%d want>=%d", jobA.SignalID, grownCap, len(step.iq))
- }
- if step.name == "config_reset_zero_new" {
- state := r.nativeState[jobA.SignalID]
- if state == nil {
- t.Fatalf("missing native state for signal %d after config reset", jobA.SignalID)
- }
- if state.historyLen != 0 {
- t.Fatalf("expected cleared native history after config reset, got=%d", state.historyLen)
- }
- }
- if step.name == "signal_b_disappears" {
- if _, ok := r.nativeState[jobB.SignalID]; ok {
- t.Fatalf("expected native state for signal %d to be removed on disappearance", jobB.SignalID)
- }
- }
- }
- }
|