Files
foxhunt/infra/k8s/argo/nsys-test-template.yaml
jgrusewski 737c2c1305 infra(argo): pin all workflow nodeSelectors to topology.kubernetes.io/zone=fr-par-2
Scaleway BSSD PVCs (cargo-target-*, sccache-*, feature-cache-pvc,
bin-cache, training-data-pvc, test-data-pvc, platform service PVCs) are
single-zone-locked at create time. The cluster's node pools in main.tf
set `region` but no `zone`, so each autoscale event picks a zone
arbitrarily. When the autoscaler spins up a node in a zone that doesn't
match an existing PVC, scheduling fails with
  "1 node(s) didn't match PersistentVolume's node affinity"
until the autoscaler eventually retries in the right zone. We hit this
on the HM pool creation (fixed manually with zone=fr-par-2) and again
on this commit's ensure-binary autoscale.

Track 1 (this commit): add `topology.kubernetes.io/zone: fr-par-2` to
every k8s.scaleway.com/pool-name nodeSelector entry across the 11 argo
workflow templates (34 entries total). The Kubernetes scheduler AND
cluster-autoscaler both honor topology keys when deciding placement /
provisioning — so future autoscale events will only spin up fr-par-2
nodes, and PVC binding is guaranteed.

Track 2 (future, structural): add `zone = "${var.region}-2"` to each
scaleway_k8s_pool in infra/modules/kapsule/main.tf so the pools never
provision in any other zone. Requires terraform-state cleanup (the
GitLab http backend currently has no states; the HM pool was created
out-of-band) and drain/recreate of any existing mixed-zone nodes —
deferred.

Verification: pool=N / paired=N coverage report shows 1:1 pool-name
to topology.kubernetes.io/zone entries in every file. kubectl apply -f
of all 11 templates returned "configured" for each.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 15:08:37 +02:00

319 lines
12 KiB
YAML

# infra/k8s/argo/nsys-test-template.yaml
#
# One-shot Nsight Systems profiling run on L40S. Wraps a smoke test under
# `nsys profile` and uploads the .nsys-rep to MinIO at
# foxhunt-training-results/profiles/smoke/<short-sha>/.
#
# Mirrors the nsys integration in train-multi-seed-template (Plan 5 Task 3,
# A.4.1) but for smoke tests. Use this for rapid kernel-timing iteration
# (per-step bottleneck hunts, SOL %, occupancy diffs) without the full
# multi-seed Argo cycle.
#
# Why L40S not local: laptop RTX 3050 Ti has 4 GB VRAM; nsys on L40S (48 GB)
# avoids any contention and runs at near-native speed (typical ~5-10%
# slowdown vs uninstrumented).
#
# Usage:
# argo submit --watch -n foxhunt nsys-test-template.yaml \
# -p commit-ref=main \
# -p test-name=performance::test_real_data_single_epoch
#
# Or via the wrapper script: scripts/argo-nsys.sh
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
name: nsys-test
namespace: foxhunt
labels:
app.kubernetes.io/name: nsys-test
app.kubernetes.io/part-of: foxhunt
spec:
entrypoint: nsys-run
serviceAccountName: argo-workflow
podMetadata:
labels:
app.kubernetes.io/part-of: foxhunt
app.kubernetes.io/component: nsys-test
archiveLogs: true
podGC:
strategy: OnPodCompletion
ttlStrategy:
secondsAfterCompletion: 7200
activeDeadlineSeconds: 3600 # nsys overhead is small — 1h cap is generous
arguments:
parameters:
- name: commit-ref
value: HEAD
- name: gpu-pool
value: ci-training-l40s
- name: cuda-compute-cap
value: "89"
- name: test-name
value: "performance::test_real_data_single_epoch"
# nsys capture options. Defaults capture the full kernel timeline.
# GPU metrics counters (--gpu-metrics-devices) require elevated CUDA
# performance-counter privileges (NVGPUCTRPERM) which the container
# lacks; CUDA/NVTX/osrt traces alone still give per-kernel timing and
# call-graph context — sufficient for the bottleneck-hunt use case.
# Override via --extra-args if running on a node with relaxed perms.
- name: nsys-extra-args
value: "--trace=cuda,nvtx,osrt"
volumes:
- name: git-ssh-key
secret:
secretName: argo-git-ssh-key
defaultMode: 256
- name: cargo-target
persistentVolumeClaim:
claimName: cargo-target-cuda-test
# Use the same training-data-pvc as production training (full 27 months
# of OHLCV + MBP-10 + trades) instead of the smaller test-data-pvc.
- name: training-data
persistentVolumeClaim:
claimName: training-data-pvc
readOnly: true
templates:
- name: nsys-run
nodeSelector:
k8s.scaleway.com/pool-name: "{{workflow.parameters.gpu-pool}}"
topology.kubernetes.io/zone: fr-par-2
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
- key: node.cilium.io/agent-not-ready
operator: Exists
effect: NoSchedule
container:
image: gitlab-registry.foxhunt.svc.cluster.local:5000/root/foxhunt/ci-builder:latest
command: ["/bin/bash", "-c"]
env:
- name: SQLX_OFFLINE
value: "true"
- name: CARGO_TERM_COLOR
value: always
- name: CARGO_TARGET_DIR
value: /cargo-target
- name: CARGO_HOME
value: /cargo-target/cargo-home
- name: CUDA_VISIBLE_DEVICES
value: "0"
- name: CUBLAS_WORKSPACE_CONFIG
value: ":4096:8"
- name: FOXHUNT_TEST_DATA
value: /data/futures-baseline
- name: TEST_DATA_DIR
value: /data/futures-baseline
# MBP-10 + trades are mandatory per feedback_mbp10_mandatory.md
# (OFI features are part of the production state vector).
- name: FOXHUNT_MBP10_DATA
value: /data/futures-baseline-mbp10
- name: FOXHUNT_TRADES_DATA
value: /data/futures-baseline-trades
- name: RUST_LOG
value: info
- name: LD_LIBRARY_PATH
value: /usr/local/nvidia/lib64:/usr/local/cuda/lib64:/usr/local/cuda/extras/CUPTI/lib64
# MinIO upload — same secret + optional pattern as train-multi-seed.
# Both refs are `optional: true` so env mount succeeds on clusters
# that do not pre-create `minio-credentials` (upload then warn-fails).
- name: MINIO_ACCESS_KEY
valueFrom:
secretKeyRef:
name: minio-credentials
key: access-key
optional: true
- name: MINIO_SECRET_KEY
valueFrom:
secretKeyRef:
name: minio-credentials
key: secret-key
optional: true
volumeMounts:
- name: git-ssh-key
mountPath: /etc/git-ssh
readOnly: true
- name: cargo-target
mountPath: /cargo-target
- name: training-data
mountPath: /data
readOnly: true
resources:
requests:
nvidia.com/gpu: "1"
cpu: "4"
memory: 32Gi
limits:
nvidia.com/gpu: "1"
cpu: "8"
memory: 64Gi
args:
- |
set -euo pipefail
REF="{{workflow.parameters.commit-ref}}"
TEST_NAME="{{workflow.parameters.test-name}}"
EXTRA_ARGS="{{workflow.parameters.nsys-extra-args}}"
echo "==================================="
echo " nsys profile L40S run"
echo "==================================="
echo " Ref: $REF"
echo " Test: $TEST_NAME"
echo " Extra: $EXTRA_ARGS"
echo " Pool: {{workflow.parameters.gpu-pool}}"
echo "==================================="
# --- SSH setup ---
mkdir -p ~/.ssh
cp /etc/git-ssh/ssh-privatekey ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf 'Host *\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null\n' > ~/.ssh/config
chmod 600 ~/.ssh/config
REPO="ssh://git@gitlab-gitlab-shell.foxhunt.svc.cluster.local:2222/root/foxhunt.git"
BUILD="/cargo-target/src"
git config --global --add safe.directory "$BUILD"
# --- Persistent checkout on PVC ---
if [ -d "$BUILD/.git" ]; then
cd "$BUILD"
git fetch origin
if git rev-parse --verify "origin/$REF" >/dev/null 2>&1; then
TARGET=$(git rev-parse "origin/$REF")
else
TARGET=$(git rev-parse --verify "$REF" 2>/dev/null || echo "$REF")
fi
CURRENT=$(git rev-parse HEAD 2>/dev/null || echo "none")
if [ "$CURRENT" != "$TARGET" ]; then
echo "=== Updating checkout to $REF ($TARGET) ==="
git checkout --force --detach "$TARGET"
git clean -fd
fi
else
git clone --filter=blob:none "$REPO" "$BUILD"
cd "$BUILD"
git fetch origin
if git rev-parse --verify "origin/$REF" >/dev/null 2>&1; then
git checkout --force --detach "origin/$REF"
else
git checkout "$REF"
fi
fi
SHA=$(git rev-parse --short HEAD)
echo "Checked out $SHA"
export PATH="${CARGO_HOME}/bin:/usr/local/cuda/bin:${PATH}"
export CUDA_COMPUTE_CAP={{workflow.parameters.cuda-compute-cap}}
bash scripts/ptx-cache-invalidate.sh "${CARGO_TARGET_DIR}/.ptx_cache"
# --- Verify nsys ---
NSYS_BIN=$(which nsys 2>/dev/null || echo /usr/local/cuda/bin/nsys)
if [ ! -x "$NSYS_BIN" ]; then
echo "ERROR: nsys not found at $NSYS_BIN"
exit 2
fi
echo "=== nsys version ==="
"$NSYS_BIN" --version | head -3
# --- Compile test binary + train_baseline_rl ---
echo "=== Compiling (--release --no-run + train_baseline_rl) ==="
cargo test -p ml --release --lib --no-run 2>&1 | tee /cargo-target/nsys-compile.log
cargo build -p ml --release --example train_baseline_rl 2>&1 | tee -a /cargo-target/nsys-compile.log
# Honour CARGO_TARGET_DIR — outputs go to ${CARGO_TARGET_DIR}/release/deps,
# NOT the repo-relative target/. (CI workflow sets CARGO_TARGET_DIR=/cargo-target.)
# Subshell + || true wraps the head -1 SIGPIPE so pipefail doesn't fire.
TARGET_DIR="${CARGO_TARGET_DIR:-target}"
TEST_BIN=$( (ls -t "$TARGET_DIR/release/deps/ml-"* 2>/dev/null \
| grep -vE '\.(d|rcgu|rmeta|rlib|so|json)$' \
| head -1) || true )
if [ -z "$TEST_BIN" ] || [ ! -x "$TEST_BIN" ]; then
echo "ERROR: could not locate compiled test binary in $TARGET_DIR/release/deps/"
ls -la "$TARGET_DIR/release/deps/" 2>&1 | head -20
exit 3
fi
echo "=== Test binary: $TEST_BIN ==="
# grep -q + pipefail = SIGPIPE on early exit. Capture once, grep
# without -q so the consumer reads all input.
TEST_LIST=$("$TEST_BIN" --list 2>&1 || true)
if ! echo "$TEST_LIST" | grep -F "$TEST_NAME" > /dev/null; then
echo "ERROR: test '$TEST_NAME' not found"
echo "$TEST_LIST" | grep smoke_tests | (head -30 || true)
exit 4
fi
# --- Run under nsys profile ---
mkdir -p /tmp/nsys-output
POD_NAME="${HOSTNAME:-pod}"
NSYS_OUT="/tmp/nsys-output/profile-${POD_NAME}.nsys-rep"
echo ""
echo "==================================="
echo " Launching nsys profile..."
echo "==================================="
START_TS=$(date +%s)
set +e
"$NSYS_BIN" profile \
--output="$NSYS_OUT" \
--force-overwrite=true \
--stats=true \
${EXTRA_ARGS} \
-- "$TEST_BIN" "$TEST_NAME" --ignored --nocapture
EXIT_CODE=$?
set -e
END_TS=$(date +%s)
ELAPSED=$((END_TS - START_TS))
echo ""
echo "==================================="
echo " nsys run complete"
echo "==================================="
echo " Exit code: $EXIT_CODE"
echo " Elapsed: ${ELAPSED}s"
ls -la "$NSYS_OUT" 2>/dev/null && \
echo " Profile: $(du -h "$NSYS_OUT" | cut -f1)"
echo ""
# --- Upload to MinIO (same pattern as train-multi-seed) ---
if [ -f "$NSYS_OUT" ]; then
echo "=== Uploading nsys profile to MinIO ==="
MC_BIN=$(which mc 2>/dev/null || echo "")
if [ -z "$MC_BIN" ]; then
MC_BIN=/tmp/mc
curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc -o "$MC_BIN" || {
echo "WARN: failed to download mc — skipping upload"
echo "Profile available locally at: $NSYS_OUT (PVC: cargo-target)"
exit 0
}
chmod +x "$MC_BIN"
fi
"$MC_BIN" alias set foxhunt http://minio.foxhunt.svc.cluster.local:9000 \
"${MINIO_ACCESS_KEY:-}" "${MINIO_SECRET_KEY:-}" 2>/dev/null || true
# Smoke profiles go to a separate prefix to keep the
# production training profile bucket clean.
UPLOAD_PATH="foxhunt/foxhunt-training-results/profiles/smoke/${SHA}/profile-${POD_NAME}.nsys-rep"
if "$MC_BIN" cp "$NSYS_OUT" "$UPLOAD_PATH"; then
echo "=== nsys profile uploaded to ${UPLOAD_PATH} ==="
echo ""
echo "Download with:"
echo " mc cp ${UPLOAD_PATH} ./profile.nsys-rep"
echo "Open with: nsys-ui profile.nsys-rep"
else
echo "WARN: nsys upload failed — profile remains on PVC at $NSYS_OUT"
fi
fi
if [ "$EXIT_CODE" -ne 0 ]; then
echo ""
echo "FAIL: test exited with code $EXIT_CODE"
exit "$EXIT_CODE"
fi
echo ""
echo "PASS: nsys profile captured, test passed."