- Compile+test in same H100 pod (eliminates cross-node PVC transfer) - New cargo-target-cuda-test PVC (30Gi) — zero contention with training - onExit notify, podGC, activeDeadlineSeconds, fsGroup, gpu-warmup - Continue-on-failure with per-model exit code capture - CUDA_COMPUTE_CAP=90, complete change detection paths - TEST_DATA_DIR marked as required prerequisite code change Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
459 lines
16 KiB
Markdown
459 lines
16 KiB
Markdown
# H100 GPU Test Workflow — Design Spec
|
|
|
|
**Date**: 2026-03-13
|
|
**Status**: Approved (rev 2 — post spec-review fixes)
|
|
**Scope**: Argo WorkflowTemplate for compile + GPU/CUDA test execution on H100
|
|
|
|
## Problem
|
|
|
|
GPU smoke tests take 42+ minutes on a 4GB RTX 3050 Ti in debug mode and OOM on multi-test concurrency. Full model coverage (DQN, PPO, 8 supervised models) is impossible locally. We need a fast, reproducible GPU test path on H100 that validates CUDA hot-path correctness with real market data.
|
|
|
|
## Decision Summary
|
|
|
|
| Decision | Choice |
|
|
|----------|--------|
|
|
| Test scope | Parameterized — all model families by default, selectable subset |
|
|
| Test data | Dedicated `test-data-pvc` (10Gi) with curated subset |
|
|
| PVC strategy | Two new PVCs — zero contention with training workflows |
|
|
| Compile strategy | Compile + test on H100 (single pod, same node = no binary transfer) |
|
|
| Triggers | Manual (active) + on-push gated (active) + nightly (suspended) |
|
|
|
|
## 1. Test Data Infrastructure
|
|
|
|
### 1.1 test-data-pvc
|
|
|
|
New PersistentVolumeClaim:
|
|
- **Name**: `test-data-pvc`
|
|
- **Namespace**: `foxhunt`
|
|
- **Size**: 10Gi
|
|
- **StorageClass**: `scw-bssd`
|
|
- **AccessMode**: `ReadWriteOnce`
|
|
|
|
Separate from `training-data-pvc` — no RWO contention with training workflows.
|
|
|
|
### 1.2 Data Layout
|
|
|
|
```
|
|
/data/test-data/
|
|
├── ohlcv/ES.FUT/ES.FUT_2025-Q2.dbn.zst # ~2MB — OHLCV bars
|
|
├── mbp10/ES.FUT/ES.FUT_2025-Q2.dbn.zst # ~126MB — orderbook snapshots
|
|
└── trades/ES.FUT/ES.FUT_2025-Q2.dbn.zst # ~300MB — tick trades
|
|
```
|
|
|
|
**~430MB total** — ES.FUT 2025-Q2 across all three schemas. Exercises every code path: feature extraction, OFI calculation, VPIN, regime classification, walk-forward windows.
|
|
|
|
### 1.3 Setup Job
|
|
|
|
`infra/k8s/training/populate-test-data-job.yaml` — one-time Kubernetes Job that:
|
|
1. Runs on H100 node pool (same node as test-data-pvc to avoid RWO cross-node)
|
|
2. Mounts `training-data-pvc` read-only at `/data/training`
|
|
3. Mounts `test-data-pvc` read-write at `/data/test-data`
|
|
4. Copies ES.FUT 2025-Q2 files from each schema directory
|
|
5. Validates file integrity (size check, dbn header parse)
|
|
|
|
**Node affinity**: Must schedule on the same node pool as the GPU test workflow (`ci-training-h100`) to ensure the RWO PVC stays on the correct node.
|
|
|
|
Re-runnable via `scripts/refresh-test-data.sh`.
|
|
|
|
### 1.4 Test Code Integration
|
|
|
|
Tests discover data via `TEST_DATA_DIR` environment variable:
|
|
- **H100 workflow**: `TEST_DATA_DIR=/data/test-data`
|
|
- **Local dev**: Falls back to existing `data/smoke-test/` relative path discovery
|
|
|
|
**Required code change**: Add `TEST_DATA_DIR` env var lookup as primary path in `try_data()` / `get_dbn_data_dir()` / `get_es_fut_data_dir()` test helpers. This is a prerequisite before the workflow can run.
|
|
|
|
## 2. Workflow Architecture
|
|
|
|
### 2.1 WorkflowTemplate: `gpu-test-pipeline`
|
|
|
|
File: `infra/k8s/argo/gpu-test-pipeline-template.yaml`
|
|
|
|
DAG structure:
|
|
```
|
|
gpu-warmup ──────────┐
|
|
├──> compile-and-test (H100, single pod)
|
|
│
|
|
notify-result (onExit, always runs)
|
|
```
|
|
|
|
**Key design decision**: Compile AND test run in the **same pod** on the H100. This eliminates:
|
|
- Cross-node PVC transfer (BLOCKER in rev 1)
|
|
- Non-deterministic test binary path discovery
|
|
- GitLab packages upload/download for test binaries
|
|
- Complexity of separating compile artifacts from test harness
|
|
|
|
Cost overhead: ~5 min of H100 time for compilation (~$0.25/run). Acceptable given the simplicity gain.
|
|
|
|
### 2.2 cargo-target-cuda-test PVC
|
|
|
|
New PersistentVolumeClaim — **independent from training's `cargo-target-cuda`**:
|
|
- **Name**: `cargo-target-cuda-test`
|
|
- **Namespace**: `foxhunt`
|
|
- **Size**: 30Gi
|
|
- **StorageClass**: `scw-bssd-retain` (survives PVC deletion, matches sccache pattern)
|
|
- **AccessMode**: `ReadWriteOnce`
|
|
|
|
Contains persistent git checkout + cargo target directory (same pattern as `cargo-target-cuda` in compile-and-train). Only changed files get new mtimes → cargo skips recompiling unchanged crates.
|
|
|
|
**Zero contention**: Training workflows use `cargo-target-cuda`, test workflows use `cargo-target-cuda-test`. Neither blocks the other.
|
|
|
|
### 2.3 Compile-and-Test Step
|
|
|
|
Single pod on `ci-training-h100`:
|
|
|
|
- **Image**: `ci-builder` (CUDA 12.9 + Rust 1.89 + mold)
|
|
- **Node pool**: `ci-training-h100`
|
|
- **Resources**:
|
|
```yaml
|
|
requests:
|
|
nvidia.com/gpu: "1"
|
|
cpu: "4"
|
|
memory: 32Gi
|
|
limits:
|
|
nvidia.com/gpu: "1"
|
|
cpu: "8"
|
|
memory: 64Gi
|
|
```
|
|
- **Tolerations**:
|
|
```yaml
|
|
- key: nvidia.com/gpu
|
|
operator: Exists
|
|
effect: NoSchedule
|
|
- key: node.cilium.io/agent-not-ready
|
|
operator: Exists
|
|
effect: NoSchedule
|
|
```
|
|
- **Volumes**:
|
|
- `cargo-target-cuda-test` PVC at `/cargo-target` — persistent checkout + incremental build
|
|
- `test-data-pvc` PVC read-only at `/data/test-data`
|
|
- `git-ssh-key` secret at `/etc/git-ssh`
|
|
- **Environment**:
|
|
```bash
|
|
SQLX_OFFLINE=true
|
|
CARGO_TARGET_DIR=/cargo-target
|
|
CARGO_HOME=/cargo-target/cargo-home
|
|
CARGO_TERM_COLOR=always
|
|
CUDA_COMPUTE_CAP=90 # H100 Hopper architecture
|
|
CUDA_VISIBLE_DEVICES=0
|
|
CUBLAS_WORKSPACE_CONFIG=:4096:8 # Deterministic cuBLAS for reproducible tests
|
|
TEST_DATA_DIR=/data/test-data
|
|
RUST_LOG=info
|
|
```
|
|
- **Execution flow**:
|
|
```bash
|
|
# 1. Checkout (persistent on PVC, incremental)
|
|
git fetch origin && git checkout --force $SHA
|
|
|
|
# 2. Compile test binaries (incremental, ~5-6 min warm)
|
|
cargo test -p ml -p ml-dqn -p ml-core --features cuda --no-run
|
|
|
|
# 3. Run tests sequentially per model (see section 4)
|
|
# Captures per-model exit codes, continues on failure
|
|
```
|
|
- **Expected duration**: ~5-6 min compile (warm) + ~15-20 min tests = **~20-25 min total**
|
|
|
|
### 2.4 GPU Warmup Step
|
|
|
|
Runs in parallel with nothing (triggers H100 autoscale while the workflow is starting):
|
|
|
|
```yaml
|
|
- name: gpu-warmup
|
|
nodeSelector:
|
|
k8s.scaleway.com/pool-name: "{{workflow.parameters.gpu-pool}}"
|
|
tolerations:
|
|
- key: nvidia.com/gpu
|
|
operator: Exists
|
|
effect: NoSchedule
|
|
- key: node.cilium.io/agent-not-ready
|
|
operator: Exists
|
|
effect: NoSchedule
|
|
container:
|
|
image: busybox:1.37
|
|
command: ["/bin/sh", "-c", "echo 'GPU warmup: node scheduled' && exit 0"]
|
|
resources:
|
|
requests:
|
|
nvidia.com/gpu: "1"
|
|
cpu: 100m
|
|
memory: 64Mi
|
|
limits:
|
|
nvidia.com/gpu: "1"
|
|
cpu: 200m
|
|
memory: 128Mi
|
|
```
|
|
|
|
Same pattern as `compile-and-train-template.yaml:265-291`. Saves 3-5 min of autoscale wait.
|
|
|
|
### 2.5 Notify Step (onExit)
|
|
|
|
Uses `onExit` handler (NOT a DAG dependency) — **always runs** regardless of test success/failure:
|
|
|
|
```yaml
|
|
onExit: notify-result
|
|
```
|
|
|
|
Same pattern as `compile-and-train-template.yaml:22` and `ci-pipeline-template.yaml:13`. Posts per-model pass/fail summary to Mattermost webhook.
|
|
|
|
### 2.6 Workflow Metadata
|
|
|
|
```yaml
|
|
securityContext:
|
|
fsGroup: 0 # Match compile-and-train
|
|
podGC:
|
|
strategy: OnPodCompletion # Match ci-pipeline
|
|
ttlStrategy:
|
|
secondsAfterCompletion: 3600 # 1 hour cleanup
|
|
activeDeadlineSeconds: 5400 # 90 min hard limit (generous for 25 min expected)
|
|
serviceAccountName: argo-workflow
|
|
```
|
|
|
|
## 3. Parameterization
|
|
|
|
| Parameter | Type | Default | Description |
|
|
|-----------|------|---------|-------------|
|
|
| `models` | string | `dqn,ppo,tft` | Comma-separated model list |
|
|
| `test-scope` | enum | `all` | `lib`, `integration`, or `all` |
|
|
| `epochs` | int | `3` | Training epochs for e2e smoke tests |
|
|
| `commit-ref` | string | `HEAD` | Git ref to compile and test (SHA, branch, or tag) |
|
|
| `gpu-pool` | string | `ci-training-h100` | GPU node pool name |
|
|
| `cuda-compute-cap` | string | `90` | CUDA compute capability (90=H100) |
|
|
| `notify` | bool | `true` | Send Mattermost notification |
|
|
|
|
**Full model list**: `dqn,ppo,tft,mamba2,tggn,tlob,liquid,kan,xlstm,diffusion`
|
|
|
|
**Special value**: `models=all` expands to the full 10-model list.
|
|
|
|
## 4. Test Matrix
|
|
|
|
### 4.1 Execution Strategy
|
|
|
|
Tests run sequentially per model family in a single pod. **Continue on failure**: each model's tests run regardless of previous model failures. Exit codes are captured per-model and aggregated at the end.
|
|
|
|
```bash
|
|
RESULTS=""
|
|
run_model_tests() {
|
|
MODEL="$1"; shift
|
|
echo "=== Testing: $MODEL ==="
|
|
"$@" --nocapture 2>&1 | tee "/tmp/test-${MODEL}.log"
|
|
EXIT=${PIPESTATUS[0]}
|
|
RESULTS="${RESULTS}${MODEL}:${EXIT}\n"
|
|
return 0 # Always continue
|
|
}
|
|
```
|
|
|
|
### 4.2 DQN Tests
|
|
|
|
```bash
|
|
# lib tests
|
|
run_model_tests dqn-lib cargo test -p ml-dqn --features cuda --lib
|
|
run_model_tests dqn-ml-lib cargo test -p ml --features cuda --lib -- dqn
|
|
|
|
# integration tests (if scope=integration or scope=all)
|
|
run_model_tests dqn-smoke cargo test -p ml --features cuda --test smoke_test_real_data
|
|
run_model_tests dqn-pipeline cargo test -p ml --features cuda --test dqn_training_pipeline_test
|
|
run_model_tests dqn-smoke-train cargo test -p ml --features cuda --test dqn_training_smoke_test
|
|
run_model_tests dqn-early-stop cargo test -p ml --features cuda --test dqn_early_stopping_termination_test
|
|
run_model_tests dqn-collapse cargo test -p ml --features cuda --test dqn_action_collapse_fix_test
|
|
```
|
|
|
|
### 4.3 PPO Tests
|
|
|
|
```bash
|
|
run_model_tests ppo-lib cargo test -p ml --features cuda --lib -- ppo
|
|
run_model_tests ppo-barrier cargo test -p ml --features cuda --test barrier_optimization_test
|
|
```
|
|
|
|
### 4.4 Supervised Model Tests
|
|
|
|
Per model (tft, mamba2, tggn, tlob, liquid, kan, xlstm, diffusion):
|
|
```bash
|
|
run_model_tests ${MODEL}-lib cargo test -p ml --features cuda --lib -- $MODEL
|
|
run_model_tests ${MODEL}-integ cargo test -p ml --features cuda --test "${MODEL}_*" 2>/dev/null || true
|
|
```
|
|
|
|
### 4.5 Shared/Core Tests
|
|
|
|
Always run regardless of model selection:
|
|
```bash
|
|
run_model_tests core cargo test -p ml-core --features cuda --lib
|
|
run_model_tests bayesian cargo test -p ml --features cuda --test bayesian_changepoint_test
|
|
```
|
|
|
|
### 4.6 Final Aggregation
|
|
|
|
```bash
|
|
# Print summary
|
|
echo "=== GPU TEST RESULTS ==="
|
|
printf "$RESULTS" | while IFS=: read model exit; do
|
|
[ "$exit" = "0" ] && STATUS="PASS" || STATUS="FAIL"
|
|
printf " %-20s %s\n" "$model" "$STATUS"
|
|
done
|
|
|
|
# Exit with failure if ANY model failed
|
|
printf "$RESULTS" | grep -v ":0$" && exit 1 || exit 0
|
|
```
|
|
|
|
## 5. Trigger Configuration
|
|
|
|
### 5.1 Manual
|
|
|
|
`scripts/argo-test.sh` — CLI wrapper for `argo submit`:
|
|
|
|
```bash
|
|
# Full suite (default)
|
|
./scripts/argo-test.sh
|
|
|
|
# Quick DQN-only
|
|
./scripts/argo-test.sh --models dqn --scope lib
|
|
|
|
# All 10 models, 5 epochs
|
|
./scripts/argo-test.sh --models all --epochs 5
|
|
|
|
# Watch logs
|
|
./scripts/argo-test.sh --models dqn,ppo --watch
|
|
```
|
|
|
|
**Status**: Active from day one.
|
|
|
|
### 5.2 On-Push (Gated)
|
|
|
|
New DAG task in `ci-pipeline-template.yaml`:
|
|
|
|
```yaml
|
|
- name: gpu-test
|
|
depends: "detect-changes"
|
|
templateRef:
|
|
name: gpu-test-pipeline
|
|
template: pipeline
|
|
arguments:
|
|
parameters:
|
|
- name: commit-ref
|
|
value: "{{workflow.parameters.commit-sha}}"
|
|
- name: models
|
|
value: "dqn,ppo,tft"
|
|
- name: epochs
|
|
value: "3"
|
|
when: "{{tasks.detect-changes.outputs.parameters.ml-changed}} == true"
|
|
```
|
|
|
|
**Change detection**: New `ml-changed` output added to `detect-changes` template. `true` when `git diff` includes files matching:
|
|
- `crates/ml/**` (includes `src/cuda_pipeline/*.cu` and `*.cuh`)
|
|
- `crates/ml-dqn/**`
|
|
- `crates/ml-core/**`
|
|
- `crates/ml-regime/**`
|
|
- `crates/ml-features/**`
|
|
- `crates/ml-supervised/**`
|
|
- `crates/ml-ppo/**`
|
|
- `crates/ml-ensemble/**`
|
|
- `crates/ml-hyperopt/**`
|
|
- `crates/ml-labeling/**`
|
|
|
|
**Implementation**: Add to the `check_paths` section of `detect-changes`:
|
|
```bash
|
|
ML_CHANGED=$(check_paths "crates/ml crates/ml-dqn crates/ml-core crates/ml-regime crates/ml-features crates/ml-supervised crates/ml-ppo crates/ml-ensemble crates/ml-hyperopt crates/ml-labeling")
|
|
echo -n "$ML_CHANGED" > /tmp/outputs/ml-changed
|
|
```
|
|
|
|
And add the output parameter:
|
|
```yaml
|
|
- name: ml-changed
|
|
valueFrom:
|
|
path: /tmp/outputs/ml-changed
|
|
```
|
|
|
|
**Status**: Active from day one. Gated — zero cost when non-ML code changes.
|
|
|
|
### 5.3 Nightly
|
|
|
|
`infra/k8s/argo/gpu-test-nightly-cron.yaml`:
|
|
|
|
```yaml
|
|
apiVersion: argoproj.io/v1alpha1
|
|
kind: CronWorkflow
|
|
metadata:
|
|
name: gpu-test-nightly
|
|
namespace: foxhunt
|
|
spec:
|
|
schedule: "0 2 * * *"
|
|
suspend: true # Created but not active — enable when ready
|
|
workflowSpec:
|
|
workflowTemplateRef:
|
|
name: gpu-test-pipeline
|
|
arguments:
|
|
parameters:
|
|
- name: models
|
|
value: "dqn,ppo,tft,mamba2,tggn,tlob,liquid,kan,xlstm,diffusion"
|
|
- name: epochs
|
|
value: "5"
|
|
- name: commit-ref
|
|
value: "main"
|
|
```
|
|
|
|
**Status**: Created but `suspend: true`. Enable with:
|
|
```bash
|
|
kubectl patch cronworkflow gpu-test-nightly -n foxhunt -p '{"spec":{"suspend":false}}'
|
|
```
|
|
|
|
## 6. File Inventory
|
|
|
|
### New files to create
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `infra/k8s/argo/gpu-test-pipeline-template.yaml` | Main WorkflowTemplate |
|
|
| `infra/k8s/argo/gpu-test-nightly-cron.yaml` | CronWorkflow (suspended) |
|
|
| `infra/k8s/argo/cargo-target-cuda-test-pvc.yaml` | 30Gi build cache PVC |
|
|
| `infra/k8s/training/test-data-pvc.yaml` | 10Gi test data PVC |
|
|
| `infra/k8s/training/populate-test-data-job.yaml` | One-time data copy job |
|
|
| `scripts/argo-test.sh` | Manual trigger CLI |
|
|
| `scripts/refresh-test-data.sh` | Re-run populate job |
|
|
|
|
### Files to modify
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `infra/k8s/argo/ci-pipeline-template.yaml` | Add `gpu-test` DAG task + `ml-changed` output to detect-changes |
|
|
| `crates/ml/tests/smoke_test_real_data.rs` | Add `TEST_DATA_DIR` env var to `try_data()` |
|
|
| `crates/ml/tests/dqn_training_pipeline_test.rs` | Add `TEST_DATA_DIR` env var to `get_es_fut_data_dir()` |
|
|
| `crates/ml/tests/dqn_training_smoke_test.rs` | Add `TEST_DATA_DIR` env var to `get_dbn_data_dir()` |
|
|
| `crates/ml/tests/dqn_early_stopping_termination_test.rs` | Add `TEST_DATA_DIR` env var to `get_dbn_data_dir()` |
|
|
|
|
## 7. Cost Estimate
|
|
|
|
| Trigger | H100 time/run | Frequency | Monthly cost estimate |
|
|
|---------|--------------|-----------|----------------------|
|
|
| Manual | ~25 min | ~5/week | ~$10 |
|
|
| On-push (gated) | ~25 min | ~10/week | ~$20 |
|
|
| Nightly (when enabled) | ~35 min | 30/month | ~$50 |
|
|
|
|
Based on Scaleway H100 pricing for `ci-training-h100` node pool. Includes ~5 min compile overhead on GPU node (simpler architecture vs CPU-compile + binary transfer).
|
|
|
|
## 8. Blocker Resolutions (from rev 1 review)
|
|
|
|
| Blocker | Resolution |
|
|
|---------|-----------|
|
|
| Workspace PVC can't cross nodes (RWO) | Eliminated: compile + test in same H100 pod |
|
|
| `cargo-target-cuda` contention | New `cargo-target-cuda-test` PVC — zero sharing |
|
|
| sccache not configured | Removed: using cargo incremental compilation (persistent checkout on PVC), matching `compile-and-train` pattern |
|
|
| Non-deterministic test binary paths | Eliminated: `cargo test` runs in same env that compiled — no binary discovery needed |
|
|
| No `activeDeadlineSeconds` | Added: 5400s (90 min) |
|
|
| No `onExit` handler | Added: `onExit: notify-result` |
|
|
| No `podGC` | Added: `OnPodCompletion` |
|
|
| Missing `fsGroup` | Added: `fsGroup: 0` |
|
|
| Missing `gpu-warmup` | Added: parallel warmup step |
|
|
| Incomplete change detection | Added: ml-ensemble, ml-hyperopt, ml-labeling paths |
|
|
| Missing `CUDA_COMPUTE_CAP` | Added: `cuda-compute-cap` parameter, default `90` |
|
|
| TEST_DATA_DIR not in test helpers | Marked as required prerequisite code change |
|
|
| No partial failure handling | Added: continue-on-failure with per-model exit code capture |
|
|
|
|
## 9. Success Criteria
|
|
|
|
1. `scripts/argo-test.sh` submits workflow and all default tests pass on H100
|
|
2. On-push trigger fires on ML crate changes, skips on non-ML changes
|
|
3. All 8 smoke_test_real_data tests pass with test-data-pvc data
|
|
4. DQN e2e training loop completes 3 epochs in <5 min (vs 42 min local)
|
|
5. Per-model pass/fail reported to Mattermost
|
|
6. No interference with training workflows (separate PVCs)
|
|
7. `activeDeadlineSeconds` kills stuck workflows within 90 min
|
|
8. Completed pods cleaned up via `podGC`
|