Files
foxhunt/scripts/gpu-hotpath-guard.sh
jgrusewski ad28482a93 fix(cuda): shmem tile overflow → CUDA_ERROR_ILLEGAL_ADDRESS on RTX 3050
Root cause: shmem_max_in_dim only included trunk dims (state_dim,
shared_h1, shared_h2) but not head dims (value_h, adv_h). When
hidden_dim_base=32 made the trunk narrow while heads stayed at 128,
the BF16 weight tile for branch output (255×128=32640 BF16 elements)
overflowed the shared memory region (12288 BF16 elements). On H100
the overflow landed in unused-but-mapped hardware shmem (silent
corruption). On RTX 3050 (48KB physical shmem) it hit unmapped
memory → CUDA_ERROR_ILLEGAL_ADDRESS.

Changes:
- gpu_dqn_trainer.rs: shmem_max_in_dim includes value_h/adv_h
- Remove all #[ignore] from smoke tests (feature_coverage,
  training_stability, gpu_residency)
- Smoke tests use real .dbn data from test_data/ (hard error if missing)
- Remove synthetic_data() fallback — no fake data in tests
- GPU-direct DtoD training path (train_step_gpu, FusedTrainScalars)
- GPU-native PER priority update kernel (zero CPU readback)
- IQN dual-head integration (gpu_iqn_head.rs)
- BF16 dtype fixes across 6 model adapters
- Hyperopt 30D→31D (iqn_lambda)
- portfolio_transformer: unconditional BF16 (remove dead CPU branches)
- liquid/adapter: all tests use Cuda(0) directly
- Fix pre-existing gpu_kernel_parity_test.rs (stale args)
- Fix pre-existing evaluate_baseline.rs (removed fields)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 08:34:51 +01:00

298 lines
11 KiB
Bash
Executable File

#!/bin/bash
# GPU Hot-Path Guard — detects CPU roundtrip leaks and GPU perf killers.
#
# Production HFT system on H100 GPUs. Every microsecond matters.
# This guard catches ALL patterns that cause GPU pipeline stalls,
# unnecessary CPU↔GPU memory transfers, or heap allocations in hot paths.
#
# Usage:
# scripts/gpu-hotpath-guard.sh <file> # check single file
# scripts/gpu-hotpath-guard.sh --staged # check all staged .rs files
# scripts/gpu-hotpath-guard.sh --diff # check unstaged changes only
# scripts/gpu-hotpath-guard.sh --all # check all hot-path .rs files
#
# Exit codes:
# 0 = clean (no leaks, or file not in hot path)
# 1 = GPU perf violation detected in hot-path file
#
# Suppress reviewed boundary points with: // gpu-ok: <reason>
# NOTE: New code should NEVER use // gpu-ok: — eliminate the transfer instead.
set -euo pipefail
# ── Hot-path file patterns ──────────────────────────────────────────────
# Only files matching these patterns are checked. Everything else is ignored.
HOT_PATHS=(
# DQN training hot loop (entire trainer directory)
"trainers/dqn/"
"cuda_pipeline/"
"gpu_action_selector"
"gpu_portfolio"
"gpu_replay_buffer"
"gpu_experience_collector"
"gpu_backtest_evaluator"
"gpu_training_guard"
"gpu_weights"
# DQN crate — all modules (inference, networks, replay, regularization)
"ml-dqn/src/"
# PPO training + inference
"trainers/ppo"
"ml-ppo/src/"
# Supervised model forward passes
"ml-supervised/src/"
# Hyperopt backtest inner loops
"hyperopt/adapters/"
"hyperopt/walk_forward.rs"
# Evaluation binaries (GPU inference paths)
"examples/evaluate_baseline.rs"
"examples/train_baseline"
# Ensemble inference
"ensemble/"
# Feature extraction (GPU-resident)
"ml-features/src/"
# Flash attention (GPU-only computation)
"flash_attention/"
)
# ── GPU perf violation patterns ────────────────────────────────────────
# Each pattern is a grep -E regex. Lines with "// gpu-ok:" are excluded.
# Doc comments (/// or //!) and Storage match arms (error guards) are excluded.
LEAK_PATTERNS=(
# ─── GPU→CPU bulk transfers (tensor array → CPU memory) ───
# Each of these forces a CUDA stream synchronize + PCIe DMA transfer.
# On H100: ~5-15μs per transfer, blocks entire GPU pipeline.
# NOTE: .to_scalar() is NOT flagged — it reads a single value (4-8 bytes),
# equivalent to the "final metric readback at epoch boundary" pattern.
'\.to_vec1'
'\.to_vec2'
'\.to_vec3'
'\.to_device\(&Device::Cpu\)'
'\.to_device\(&candle_core::Device::Cpu\)'
'\.get\([0-9]+\)\?\.to_vec'
# ─── CPU device usage (creating anything on CPU in GPU code) ───
'&Device::Cpu'
# ─── Dead GPU transfers (underscore-hidden variables suppress clippy) ───
# let _foo = tensor.to_vec1 — forced GPU→CPU sync, result discarded.
'let _\w+\s*=.*\.to_vec[123]'
# ─── Dead CPU fallback paths ───
# #[cfg(not(feature = "cuda"))] blocks are dead code — project is CUDA-only.
# They add maintenance burden and hide the real code path.
'#\[cfg\(not\(feature\s*=\s*"cuda"\)\)\]'
# ─── CPU-side batch data mutation (GPU data wrongly processed on CPU) ───
# Functions taking mutable Vec<f32> batch arrays indicate CPU-side processing
# that should be a GPU kernel or Candle tensor ops.
# Caught: CPU HER relabeling, CPU normalization, CPU reward shaping, etc.
'mut states: Vec<f32>'
'mut next_states: Vec<f32>'
'mut rewards: Vec<f32>'
# ─── Candle tensor wrapping in per-step hot path ───
# Creating Candle Tensors from CudaSlice in the training loop adds
# Rust-side allocation + dispatch overhead. Use raw CUDA kernels instead.
# Tensor::new() creates a CPU scalar → GPU copy (HtoD for 4 bytes).
# CudaStorage::wrap_cuda_slice takes ownership — blocks reuse patterns.
'CudaStorage::wrap_cuda_slice'
# NOTE: Tensor::from_vec/from_slice are NOT flagged — they are CPU→GPU
# uploads (correct direction). Inner-loop abuse is caught by code review.
#
# NOTE: memcpy_dtoh/htod and .synchronize() are not checked here —
# they are CUDA implementation primitives in cuda_pipeline/.
# Audit those separately with: scripts/cuda-perf-audit.sh
#
# NOTE: storage_and_layout() is NOT flagged — it's a zero-cost pointer
# unwrap for extracting CudaSlice refs from existing Tensors.
)
# ── Excluded paths ────────────────────────────────────────────────────
EXCLUDE_PATHS=(
# One-time data loading at startup, not per-step hot path
"trainers/dqn/data_loading.rs"
# Smoke tests (not training hot path)
"trainers/dqn/smoke_tests/"
# Legacy non-branching QNetwork — CPU API boundary, not production hot path
"ml-dqn/src/network.rs"
"ml-dqn/src/ensemble_network.rs"
# Model inference boundaries — return CPU types by design (EnsemblePrediction)
# Training hot path uses GPU-resident tensors directly
"ml-supervised/src/tft/varmap_quantization.rs"
# tensor_to_predictions() returns Vec<Price> — inference output boundary
"ml-supervised/src/tft/hft_optimizations.rs"
# evaluate_cpu() is slow fallback before precompute_gpu_grid(); GPU path exists
"ml-supervised/src/kan/spline.rs"
# SearchSorted/CumsumOp CustomOp2 impls must return CudaStorage (Candle API boundary)
# Runs once per PER sampling batch, not inside per-step training kernel loop
"ml-dqn/src/gpu_replay_buffer.rs"
)
# ── Test module exclusion ─────────────────────────────────────────────
# Unit tests (#[cfg(test)] modules) are excluded — assertions inherently
# need scalar readbacks. Use .to_scalar() in tests (not flagged).
# Integration tests (crates/ml/tests/) ARE checked via HOT_PATHS.
find_test_module_start() {
local file="$1"
grep -n '^\s*#\[cfg(test)\]' "$file" 2>/dev/null | head -1 | cut -d: -f1
}
is_hot_path() {
local file="$1"
# Check exclusions first
for excl in "${EXCLUDE_PATHS[@]}"; do
if [[ "$file" == *"$excl"* ]]; then
return 1
fi
done
for pattern in "${HOT_PATHS[@]}"; do
if [[ "$file" == *"$pattern"* ]]; then
return 0
fi
done
return 1
}
check_file() {
local file="$1"
local found=0
if ! is_hot_path "$file"; then
return 0
fi
# Find where #[cfg(test)] starts — skip all lines at or after that point
local test_start
test_start=$(find_test_module_start "$file")
for pattern in "${LEAK_PATTERNS[@]}"; do
# Find matches, exclude: doc comments, Storage match arms, regular comments
local hits
hits=$(grep -nE "$pattern" "$file" 2>/dev/null \
| grep -v '[0-9]*:\s*///' \
| grep -v '[0-9]*:\s*//!' \
| grep -v '[0-9]*:\s*//' \
| grep -v 'Storage::Cpu' \
| grep -v 'Storage::Metal' \
|| true)
# Filter out lines inside #[cfg(test)] module
if [ -n "$test_start" ] && [ -n "$hits" ]; then
hits=$(echo "$hits" | while IFS= read -r line; do
local lineno
lineno=$(echo "$line" | cut -d: -f1)
if [ "$lineno" -lt "$test_start" ]; then
echo "$line"
fi
done)
fi
# Filter out #[cfg(not(feature = "cuda"))] guarded code (dead code with CUDA).
# Tracks brace depth to skip entire functions/blocks, not just 2 lines.
if [ -n "$hits" ]; then
local skip_ranges
skip_ranges=$(python3 -c "
import re, sys
lines = open(sys.argv[1]).readlines()
ranges = []
for i, line in enumerate(lines):
if re.search(r'#\[cfg\(not\(feature.*cuda', line):
start = i
depth = 0
found_open = False
for j in range(i, len(lines)):
for ch in lines[j]:
if ch == '{':
depth += 1
found_open = True
elif ch == '}':
depth -= 1
if found_open and depth == 0:
ranges.append((start + 1, j + 1)) # 1-indexed
break
else:
# Single expression (no braces): skip attribute + next line
ranges.append((start + 1, min(start + 2, len(lines))))
for s, e in ranges:
print(f'{s} {e}')
" "$file" 2>/dev/null || true)
if [ -n "$skip_ranges" ]; then
hits=$(echo "$hits" | while IFS= read -r line; do
local lineno
lineno=$(echo "$line" | cut -d: -f1)
local skip=false
while IFS=' ' read -r range_start range_end; do
if [ "$lineno" -ge "$range_start" ] && [ "$lineno" -le "$range_end" ]; then
skip=true
break
fi
done <<< "$skip_ranges"
if [ "$skip" = false ]; then
echo "$line"
fi
done)
fi
fi
if [ -n "$hits" ]; then
if [ "$found" -eq 0 ]; then
echo "GPU HOT-PATH LEAK: $file"
found=1
fi
echo "$hits" | while IFS= read -r line; do
echo " $line"
done
fi
done
return $found
}
# ── Entrypoint ──────────────────────────────────────────────────────────
FILES=()
MODE="file"
if [ "${1:-}" = "--staged" ]; then
MODE="staged"
while IFS= read -r f; do
FILES+=("$f")
done < <(git diff --cached --name-only --diff-filter=ACM | grep '\.rs$' || true)
elif [ "${1:-}" = "--diff" ]; then
MODE="diff"
while IFS= read -r f; do
FILES+=("$f")
done < <(git diff --name-only | grep '\.rs$' || true)
elif [ "${1:-}" = "--all" ]; then
MODE="all"
while IFS= read -r f; do
FILES+=("$f")
done < <(find crates -name '*.rs' -type f 2>/dev/null)
elif [ -n "${1:-}" ]; then
FILES=("$1")
else
echo "Usage: $0 <file|--staged|--diff|--all>"
exit 0
fi
LEAKED=0
LEAK_COUNT=0
for file in "${FILES[@]}"; do
if [ -f "$file" ]; then
if ! check_file "$file"; then
LEAKED=1
LEAK_COUNT=$((LEAK_COUNT + 1))
fi
fi
done
if [ "$LEAKED" -eq 1 ]; then
echo ""
echo "$LEAK_COUNT file(s) with GPU perf violations in hot paths."
echo " MUST be eliminated — do not suppress with // gpu-ok:"
echo " Move transfers out of hot paths or keep data GPU-resident."
exit 1
fi
exit 0