Files
foxhunt/scripts/gpu-hotpath-guard.sh
jgrusewski 979f135271 fix(cuda): add BF16 input casts to forward methods after mixed_precision removal
After removing ensure_training_dtype(), forward methods that receive F32
inputs from tests/callers now fail with dtype mismatch against BF16 weights.
Add to_dtype(BF16) at forward entry of xLSTM (slstm, mlstm, block, network),
CfC cell, and diffusion time embedding. Fix quantization to accept BF16
tensors by casting to F32 before INT8 conversion. Update guard to catch
#[cfg(not(feature = "cuda"))] dead code. Bump DQN emergency_safe_defaults
replay_buffer_capacity from 1000 to 2048 (GPU PER floor = 1024).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 16:46:43 +01:00

277 lines
9.9 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"\)\)\]'
# 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
)
# ── 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"
)
# ── 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