hyperopt_baseline_rl uses --output (JSON file path), while train_baseline_rl uses --output-dir (checkpoint directory). Split arg generation by preset. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
720 lines
25 KiB
Bash
Executable File
720 lines
25 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# train.sh - Training orchestrator: submits K8s Jobs to the GPU pool
|
||
# ---------------------------------------------------------------------------
|
||
# Usage:
|
||
# ./infra/scripts/train.sh <preset> [--model MODEL] [--symbol SYMBOL]
|
||
# [--trials N] [--max-steps N] [--timeout N]
|
||
# [--commit SHA] [--poll-interval N]
|
||
#
|
||
# Presets:
|
||
# quick-test - fast sanity check (max-steps=500, requires --model)
|
||
# single-model - full training run for one model (requires --model)
|
||
# full-ensemble - trains all 10 models sequentially (dqn ppo tft mamba2 tggn tlob liquid kan xlstm diffusion)
|
||
# hyperopt - hyperparameter optimisation (requires --model, uses --trials)
|
||
# ci-train - wait for Argo CI to pass, then auto-submit training (requires --model)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# --- Defaults -------------------------------------------------------------
|
||
SYMBOL="ES.FUT"
|
||
TRIALS=20
|
||
MAX_STEPS=2000
|
||
TIMEOUT=3600 # default; hyperopt preset overrides to 6h below
|
||
DATA_DIR="/data/futures-baseline"
|
||
NAMESPACE="foxhunt"
|
||
IMAGE="gitlab-registry.foxhunt.svc.cluster.local:5000/root/foxhunt/foxhunt-training-runtime:latest"
|
||
MODEL=""
|
||
PRESET=""
|
||
COMMIT=""
|
||
POLL_INTERVAL=30
|
||
TRAIN_PRESET="hyperopt"
|
||
TIMESTAMP="$(date +%s)"
|
||
S3_BUCKET="foxhunt-models"
|
||
MINIO_ENDPOINT="http://minio.foxhunt.svc.cluster.local:9000"
|
||
RUN_ID="$(date +%Y%m%d-%H%M%S)"
|
||
|
||
ALL_MODELS=(dqn ppo tft mamba2 tggn tlob liquid kan xlstm diffusion)
|
||
|
||
# --- Model-to-binary mapping ----------------------------------------------
|
||
declare -A MODEL_BINARY=(
|
||
[dqn]=train_baseline_rl
|
||
[ppo]=train_baseline_rl
|
||
[tft]=train_baseline_supervised
|
||
[mamba2]=train_baseline_supervised
|
||
[tggn]=train_baseline_supervised
|
||
[tlob]=train_baseline_supervised
|
||
[liquid]=train_baseline_supervised
|
||
[kan]=train_baseline_supervised
|
||
[xlstm]=train_baseline_supervised
|
||
[diffusion]=train_baseline_supervised
|
||
)
|
||
|
||
# Hyperopt uses separate binaries
|
||
declare -A HYPEROPT_BINARY=(
|
||
[dqn]=hyperopt_baseline_rl
|
||
[ppo]=hyperopt_baseline_rl
|
||
[tft]=hyperopt_baseline_supervised
|
||
[mamba2]=hyperopt_baseline_supervised
|
||
[tggn]=hyperopt_baseline_supervised
|
||
[tlob]=hyperopt_baseline_supervised
|
||
[liquid]=hyperopt_baseline_supervised
|
||
[kan]=hyperopt_baseline_supervised
|
||
[xlstm]=hyperopt_baseline_supervised
|
||
[diffusion]=hyperopt_baseline_supervised
|
||
)
|
||
|
||
EVAL_BINARY="evaluate_baseline"
|
||
|
||
# --- Helpers --------------------------------------------------------------
|
||
usage() {
|
||
cat <<'USAGE'
|
||
Usage: ./infra/scripts/train.sh <preset> [OPTIONS]
|
||
|
||
Presets:
|
||
quick-test Fast sanity check (requires --model, max-steps=500)
|
||
single-model Full training run (requires --model)
|
||
full-ensemble Trains all 10 models sequentially (dqn ppo tft mamba2 tggn tlob liquid kan xlstm diffusion)
|
||
hyperopt Hyperparameter search (requires --model, uses --trials)
|
||
evaluate Evaluate trained models (requires --run-id from prior training run)
|
||
ci-train Wait for Argo CI pipeline, then auto-submit training (requires --model)
|
||
|
||
Options:
|
||
--model MODEL Model name: dqn | ppo | tft | mamba2 | tggn | tlob | liquid | kan | xlstm | diffusion
|
||
--symbol SYMBOL Trading symbol (default: ES.FUT)
|
||
--trials N Hyperopt trial count (default: 20)
|
||
--max-steps N Max steps per epoch (default: 2000)
|
||
--timeout N Job timeout in seconds (default: 3600)
|
||
--commit SHA Git commit to track CI for (default: HEAD of main)
|
||
--poll-interval N CI poll interval seconds (default: 30)
|
||
--train-preset P Training preset after CI (default: hyperopt; for ci-train)
|
||
-h, --help Show this help message
|
||
USAGE
|
||
exit 0
|
||
}
|
||
|
||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||
|
||
validate_model() {
|
||
local m="$1"
|
||
[[ -n "${MODEL_BINARY[$m]+x}" ]] || die "Unknown model '${m}'. Valid: ${!MODEL_BINARY[*]}"
|
||
}
|
||
|
||
# --- Parse arguments -------------------------------------------------------
|
||
[[ $# -eq 0 ]] && usage
|
||
[[ "$1" == "-h" || "$1" == "--help" ]] && usage
|
||
|
||
PRESET="$1"; shift
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--model) MODEL="$2"; shift 2 ;;
|
||
--symbol) SYMBOL="$2"; shift 2 ;;
|
||
--trials) TRIALS="$2"; shift 2 ;;
|
||
--max-steps) MAX_STEPS="$2"; shift 2 ;;
|
||
--timeout) TIMEOUT="$2"; shift 2 ;;
|
||
--run-id) RUN_ID="$2"; shift 2 ;;
|
||
--commit) COMMIT="$2"; shift 2 ;;
|
||
--poll-interval) POLL_INTERVAL="$2"; shift 2 ;;
|
||
--train-preset) TRAIN_PRESET="$2"; shift 2 ;;
|
||
-h|--help) usage ;;
|
||
*) die "Unknown option: $1" ;;
|
||
esac
|
||
done
|
||
|
||
# --- Preset validation -----------------------------------------------------
|
||
case "$PRESET" in
|
||
quick-test)
|
||
[[ -z "$MODEL" ]] && die "quick-test preset requires --model"
|
||
validate_model "$MODEL"
|
||
MAX_STEPS=500
|
||
;;
|
||
single-model)
|
||
[[ -z "$MODEL" ]] && die "single-model preset requires --model"
|
||
validate_model "$MODEL"
|
||
;;
|
||
full-ensemble)
|
||
# Trains all models; --model is ignored if provided
|
||
MODEL=""
|
||
;;
|
||
hyperopt)
|
||
[[ -z "$MODEL" ]] && die "hyperopt preset requires --model"
|
||
validate_model "$MODEL"
|
||
# Hyperopt runs 20 trials × 8 epochs — needs 6h unless user specified --timeout
|
||
if [[ "$TIMEOUT" -eq 3600 ]]; then
|
||
TIMEOUT=21600
|
||
fi
|
||
;;
|
||
evaluate)
|
||
[[ -z "$RUN_ID" ]] && die "evaluate preset requires --run-id (from a previous training run)"
|
||
;;
|
||
ci-train)
|
||
[[ -z "$MODEL" ]] && die "ci-train preset requires --model"
|
||
validate_model "$MODEL"
|
||
# Resolve commit SHA if not provided
|
||
if [[ -z "$COMMIT" ]]; then
|
||
COMMIT="$(git rev-parse HEAD 2>/dev/null || echo "")"
|
||
[[ -z "$COMMIT" ]] && die "Could not determine commit SHA. Use --commit SHA"
|
||
fi
|
||
;;
|
||
*)
|
||
die "Unknown preset '${PRESET}'. Valid: quick-test, single-model, full-ensemble, hyperopt, evaluate, ci-train"
|
||
;;
|
||
esac
|
||
|
||
|
||
# --- CI Monitoring --------------------------------------------------------
|
||
# Find the Argo Workflow for a given commit SHA.
|
||
# Argo CI workflows are submitted with parameter commit-sha=<sha>.
|
||
# We search for workflows matching the ci-pipeline template.
|
||
find_ci_workflow() {
|
||
local sha="$1"
|
||
local short_sha="${sha:0:8}"
|
||
# List recent ci-pipeline workflows and find the one matching our commit.
|
||
# Argo Events labels workflows with events.argoproj.io/trigger (not
|
||
# workflows.argoproj.io/workflow-template), so use that selector.
|
||
kubectl -n "${NAMESPACE}" get workflows \
|
||
-l "events.argoproj.io/trigger=ci-pipeline" \
|
||
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.phase}{"\t"}{range .spec.arguments.parameters[*]}{.name}={.value}{","}{end}{"\n"}{end}' \
|
||
2>/dev/null \
|
||
| grep "commit-sha=${sha}\|commit-sha=${short_sha}" \
|
||
| head -1 \
|
||
| cut -f1
|
||
}
|
||
|
||
# Get workflow phase: Pending, Running, Succeeded, Failed, Error
|
||
get_workflow_phase() {
|
||
local wf_name="$1"
|
||
kubectl -n "${NAMESPACE}" get workflow "${wf_name}" \
|
||
-o jsonpath='{.status.phase}' 2>/dev/null
|
||
}
|
||
|
||
# Get workflow node statuses (one-liner summary)
|
||
get_workflow_summary() {
|
||
local wf_name="$1"
|
||
kubectl -n "${NAMESPACE}" get workflow "${wf_name}" \
|
||
-o jsonpath='{range .status.nodes[*]}{.displayName}: {.phase} {end}' 2>/dev/null
|
||
}
|
||
|
||
# Get workflow start time and duration
|
||
get_workflow_timing() {
|
||
local wf_name="$1"
|
||
local started finished
|
||
started=$(kubectl -n "${NAMESPACE}" get workflow "${wf_name}" \
|
||
-o jsonpath='{.status.startedAt}' 2>/dev/null)
|
||
finished=$(kubectl -n "${NAMESPACE}" get workflow "${wf_name}" \
|
||
-o jsonpath='{.status.finishedAt}' 2>/dev/null)
|
||
if [[ -n "$finished" && "$finished" != "null" ]]; then
|
||
echo "started=${started} finished=${finished}"
|
||
elif [[ -n "$started" ]]; then
|
||
echo "started=${started} (still running)"
|
||
else
|
||
echo "(not started yet)"
|
||
fi
|
||
}
|
||
|
||
# Print Prometheus / log links for CI monitoring
|
||
print_monitor_links() {
|
||
local wf_name="$1"
|
||
echo ""
|
||
echo " Argo UI: kubectl -n ${NAMESPACE} get workflow ${wf_name} -o yaml"
|
||
echo " Logs: kubectl -n ${NAMESPACE} logs -l workflows.argoproj.io/workflow=${wf_name} --prefix -f"
|
||
echo " Prometheus: http://prometheus.foxhunt.svc.cluster.local:9090/graph?g0.expr=argo_workflow_status_phase"
|
||
echo ""
|
||
}
|
||
|
||
# Wait for CI workflow to complete. Returns 0 on success, 1 on failure.
|
||
wait_for_ci() {
|
||
local sha="$1"
|
||
local short_sha="${sha:0:8}"
|
||
local wf_name=""
|
||
local phase=""
|
||
local attempt=0
|
||
local max_wait_for_wf=60 # max attempts to find the workflow (60 * poll = 30 min)
|
||
|
||
echo "======================================================================"
|
||
echo " Waiting for Argo CI pipeline to complete"
|
||
echo " Commit : ${sha} (${short_sha})"
|
||
echo " Poll : every ${POLL_INTERVAL}s"
|
||
echo "======================================================================"
|
||
echo ""
|
||
|
||
# Phase 1: Find the workflow (it may not exist yet if webhook is delayed)
|
||
echo "--- Looking for CI workflow for commit ${short_sha}..."
|
||
while [[ -z "$wf_name" ]]; do
|
||
wf_name=$(find_ci_workflow "$sha")
|
||
if [[ -n "$wf_name" ]]; then
|
||
echo " Found workflow: ${wf_name}"
|
||
print_monitor_links "$wf_name"
|
||
break
|
||
fi
|
||
attempt=$((attempt + 1))
|
||
if [[ $attempt -ge $max_wait_for_wf ]]; then
|
||
echo "ERROR: No CI workflow found for commit ${short_sha} after $((attempt * POLL_INTERVAL))s"
|
||
echo " Verify the GitLab webhook triggered. Check:"
|
||
echo " kubectl -n ${NAMESPACE} get workflows -l workflows.argoproj.io/workflow-template=ci-pipeline"
|
||
return 1
|
||
fi
|
||
echo " Workflow not found yet (attempt ${attempt}/${max_wait_for_wf}), retrying in ${POLL_INTERVAL}s..."
|
||
sleep "${POLL_INTERVAL}"
|
||
done
|
||
|
||
# Phase 2: Poll until terminal phase
|
||
echo "--- Monitoring workflow ${wf_name}..."
|
||
while true; do
|
||
phase=$(get_workflow_phase "$wf_name")
|
||
local timing
|
||
timing=$(get_workflow_timing "$wf_name")
|
||
|
||
case "$phase" in
|
||
Succeeded)
|
||
echo ""
|
||
echo " CI PASSED (${timing})"
|
||
echo ""
|
||
return 0
|
||
;;
|
||
Failed|Error)
|
||
echo ""
|
||
echo " CI FAILED phase=${phase} (${timing})"
|
||
echo ""
|
||
echo " Workflow summary:"
|
||
get_workflow_summary "$wf_name" | tr ' ' '\n' | sed 's/^/ /'
|
||
echo ""
|
||
echo " Logs: kubectl -n ${NAMESPACE} logs -l workflows.argoproj.io/workflow=${wf_name} --prefix"
|
||
return 1
|
||
;;
|
||
Running|Pending|"")
|
||
local step_info
|
||
step_info=$(get_workflow_summary "$wf_name" 2>/dev/null | tr ' ' '\n' | grep -c "Running" || echo "0")
|
||
printf " [%s] phase=%-8s running_steps=%s %s\n" \
|
||
"$(date +%H:%M:%S)" "${phase:-Pending}" "${step_info}" "${timing}"
|
||
;;
|
||
*)
|
||
echo " [$(date +%H:%M:%S)] phase=${phase} (unexpected)"
|
||
;;
|
||
esac
|
||
|
||
sleep "${POLL_INTERVAL}"
|
||
done
|
||
}
|
||
|
||
# --- Generate K8s Job manifest --------------------------------------------
|
||
generate_job_manifest() {
|
||
local model="$1"
|
||
local job_name="train-${model}-${TIMESTAMP}"
|
||
local binary
|
||
if [[ "$PRESET" == "hyperopt" ]]; then
|
||
binary="${HYPEROPT_BINARY[$model]}"
|
||
else
|
||
binary="${MODEL_BINARY[$model]}"
|
||
fi
|
||
|
||
# Build CLI args (excluding binary name)
|
||
local cli_args=("--model" "$model" "--symbol" "$SYMBOL" "--data-dir" "/data/futures-baseline")
|
||
cli_args+=("--mbp10-data-dir" "/data/futures-baseline-mbp10")
|
||
cli_args+=("--trades-data-dir" "/data/futures-baseline-trades")
|
||
if [[ "$PRESET" == "hyperopt" ]]; then
|
||
# hyperopt binary: --output is a JSON file path, --trials count
|
||
cli_args+=("--output" "/output/hyperopt_results.json")
|
||
cli_args+=("--trials" "$TRIALS")
|
||
else
|
||
# training binary: --output-dir is a directory for checkpoints
|
||
cli_args+=("--output-dir" "/output")
|
||
fi
|
||
|
||
# Convert args array to YAML list items
|
||
local args_yaml=""
|
||
for arg in "${cli_args[@]}"; do
|
||
args_yaml="${args_yaml}
|
||
- \"${arg}\""
|
||
done
|
||
|
||
cat <<EOF
|
||
apiVersion: batch/v1
|
||
kind: Job
|
||
metadata:
|
||
name: ${job_name}
|
||
namespace: ${NAMESPACE}
|
||
labels:
|
||
foxhunt/job-type: training
|
||
foxhunt/model: ${model}
|
||
foxhunt/preset: ${PRESET}
|
||
foxhunt/run-id: "${RUN_ID}"
|
||
spec:
|
||
activeDeadlineSeconds: ${TIMEOUT}
|
||
backoffLimit: 1
|
||
ttlSecondsAfterFinished: 600
|
||
template:
|
||
metadata:
|
||
annotations:
|
||
prometheus.io/scrape: "true"
|
||
prometheus.io/port: "9094"
|
||
prometheus.io/path: "/metrics"
|
||
labels:
|
||
app.kubernetes.io/part-of: foxhunt
|
||
app.kubernetes.io/component: training-workflow
|
||
foxhunt/job-type: training
|
||
foxhunt/model: ${model}
|
||
foxhunt/preset: ${PRESET}
|
||
spec:
|
||
restartPolicy: Never
|
||
nodeSelector:
|
||
k8s.scaleway.com/pool-name: ci-training-h100
|
||
tolerations:
|
||
- key: nvidia.com/gpu
|
||
operator: Exists
|
||
effect: NoSchedule
|
||
- key: node.cilium.io/agent-not-ready
|
||
operator: Exists
|
||
effect: NoSchedule
|
||
imagePullSecrets:
|
||
- name: gitlab-registry
|
||
initContainers:
|
||
- name: fetch-binary
|
||
image: ${IMAGE}
|
||
command: ["/bin/sh", "-c"]
|
||
args:
|
||
- |
|
||
set -e
|
||
BINARY="${binary}"
|
||
curl -fSL -o "/binaries/\${BINARY}" \\
|
||
--header "DEPLOY-TOKEN: \${GITLAB_DEPLOY_TOKEN}" \\
|
||
"\${GITLAB_API}/projects/1/packages/generic/foxhunt-training/\${FOXHUNT_RELEASE}/\${BINARY}"
|
||
chmod +x "/binaries/\${BINARY}"
|
||
echo "Fetched \${BINARY} \${FOXHUNT_RELEASE} (\$(stat -c%s /binaries/\${BINARY}) bytes)"
|
||
env:
|
||
- name: GITLAB_DEPLOY_TOKEN
|
||
valueFrom:
|
||
secretKeyRef:
|
||
name: gitlab-deploy-token
|
||
key: token
|
||
- name: GITLAB_API
|
||
value: "http://gitlab-webservice-default.foxhunt.svc.cluster.local:8181/api/v4"
|
||
- name: FOXHUNT_RELEASE
|
||
value: "latest"
|
||
volumeMounts:
|
||
- name: binaries
|
||
mountPath: /binaries
|
||
resources:
|
||
requests:
|
||
cpu: 100m
|
||
memory: 64Mi
|
||
limits:
|
||
cpu: 500m
|
||
memory: 128Mi
|
||
- name: sync-training-data
|
||
image: ${IMAGE}
|
||
command: ["/bin/sh", "-c"]
|
||
args:
|
||
- |
|
||
set -e
|
||
RCLONE_FLAGS="--s3-provider=Minio --s3-endpoint=http://minio.foxhunt.svc.cluster.local:9000 --s3-access-key-id=\${MINIO_ACCESS_KEY} --s3-secret-access-key=\${MINIO_SECRET_KEY} --s3-no-check-bucket"
|
||
echo "Syncing OHLCV data..."
|
||
rclone sync ":s3:foxhunt-training-data/futures-baseline" /data/futures-baseline \$RCLONE_FLAGS --stats-one-line -v
|
||
echo "Syncing MBP-10 data..."
|
||
rclone sync ":s3:foxhunt-training-data/futures-baseline-mbp10" /data/futures-baseline-mbp10 \$RCLONE_FLAGS --stats-one-line -v
|
||
echo "Syncing trades data..."
|
||
rclone sync ":s3:foxhunt-training-data/futures-baseline-trades" /data/futures-baseline-trades \$RCLONE_FLAGS --stats-one-line -v
|
||
echo "Data sync complete:"
|
||
du -sh /data/futures-baseline/ /data/futures-baseline-mbp10/ /data/futures-baseline-trades/ 2>/dev/null || true
|
||
env:
|
||
- name: MINIO_ACCESS_KEY
|
||
valueFrom:
|
||
secretKeyRef:
|
||
name: minio-credentials
|
||
key: access-key
|
||
- name: MINIO_SECRET_KEY
|
||
valueFrom:
|
||
secretKeyRef:
|
||
name: minio-credentials
|
||
key: secret-key
|
||
volumeMounts:
|
||
- name: training-data
|
||
mountPath: /data
|
||
resources:
|
||
requests:
|
||
cpu: "1"
|
||
memory: 512Mi
|
||
limits:
|
||
cpu: "2"
|
||
memory: 1Gi
|
||
containers:
|
||
- name: training
|
||
image: ${IMAGE}
|
||
command: ["/binaries/${binary}"]
|
||
args:${args_yaml}
|
||
env:
|
||
- name: RUST_LOG
|
||
value: info
|
||
- name: SQLX_OFFLINE
|
||
value: "true"
|
||
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
||
value: "http://tempo.foxhunt.svc.cluster.local:4317"
|
||
resources:
|
||
requests:
|
||
nvidia.com/gpu: "1"
|
||
cpu: "4"
|
||
memory: "16Gi"
|
||
limits:
|
||
nvidia.com/gpu: "1"
|
||
cpu: "8"
|
||
memory: "32Gi"
|
||
volumeMounts:
|
||
- name: training-data
|
||
mountPath: /data
|
||
readOnly: true
|
||
- name: output
|
||
mountPath: /output
|
||
- name: binaries
|
||
mountPath: /binaries
|
||
readOnly: true
|
||
volumes:
|
||
- name: training-data
|
||
persistentVolumeClaim:
|
||
claimName: training-data-pvc
|
||
- name: output
|
||
emptyDir:
|
||
sizeLimit: 2Gi
|
||
- name: binaries
|
||
emptyDir:
|
||
sizeLimit: 500Mi
|
||
EOF
|
||
}
|
||
|
||
generate_eval_manifest() {
|
||
local job_name="eval-${RUN_ID}-${TIMESTAMP}"
|
||
|
||
cat <<EOF
|
||
apiVersion: batch/v1
|
||
kind: Job
|
||
metadata:
|
||
name: ${job_name}
|
||
namespace: ${NAMESPACE}
|
||
labels:
|
||
foxhunt/job-type: evaluate
|
||
foxhunt/run-id: "${RUN_ID}"
|
||
spec:
|
||
activeDeadlineSeconds: ${TIMEOUT}
|
||
backoffLimit: 1
|
||
ttlSecondsAfterFinished: 600
|
||
template:
|
||
metadata:
|
||
labels:
|
||
app.kubernetes.io/part-of: foxhunt
|
||
app.kubernetes.io/component: training-workflow
|
||
foxhunt/job-type: evaluate
|
||
annotations:
|
||
prometheus.io/scrape: "true"
|
||
prometheus.io/port: "9094"
|
||
prometheus.io/path: "/metrics"
|
||
spec:
|
||
restartPolicy: Never
|
||
nodeSelector:
|
||
k8s.scaleway.com/pool-name: ci-training-h100
|
||
tolerations:
|
||
- key: nvidia.com/gpu
|
||
operator: Exists
|
||
effect: NoSchedule
|
||
- key: node.cilium.io/agent-not-ready
|
||
operator: Exists
|
||
effect: NoSchedule
|
||
imagePullSecrets:
|
||
- name: gitlab-registry
|
||
initContainers:
|
||
- name: fetch-binary
|
||
image: ${IMAGE}
|
||
command: ["/bin/sh", "-c"]
|
||
args:
|
||
- |
|
||
set -e
|
||
BINARY="${EVAL_BINARY}"
|
||
curl -fSL -o "/binaries/\${BINARY}" \\
|
||
--header "DEPLOY-TOKEN: \${GITLAB_DEPLOY_TOKEN}" \\
|
||
"\${GITLAB_API}/projects/1/packages/generic/foxhunt-training/\${FOXHUNT_RELEASE}/\${BINARY}"
|
||
chmod +x "/binaries/\${BINARY}"
|
||
echo "Fetched \${BINARY} \${FOXHUNT_RELEASE} (\$(stat -c%s /binaries/\${BINARY}) bytes)"
|
||
env:
|
||
- name: GITLAB_DEPLOY_TOKEN
|
||
valueFrom:
|
||
secretKeyRef:
|
||
name: gitlab-deploy-token
|
||
key: token
|
||
- name: GITLAB_API
|
||
value: "http://gitlab-webservice-default.foxhunt.svc.cluster.local:8181/api/v4"
|
||
- name: FOXHUNT_RELEASE
|
||
value: "latest"
|
||
volumeMounts:
|
||
- name: binaries
|
||
mountPath: /binaries
|
||
resources:
|
||
requests:
|
||
cpu: 100m
|
||
memory: 64Mi
|
||
limits:
|
||
cpu: 500m
|
||
memory: 128Mi
|
||
- name: sync-training-data
|
||
image: ${IMAGE}
|
||
command: ["/bin/sh", "-c"]
|
||
args:
|
||
- |
|
||
set -e
|
||
RCLONE_FLAGS="--s3-provider=Minio --s3-endpoint=http://minio.foxhunt.svc.cluster.local:9000 --s3-access-key-id=\${MINIO_ACCESS_KEY} --s3-secret-access-key=\${MINIO_SECRET_KEY} --s3-no-check-bucket"
|
||
echo "Syncing OHLCV data..."
|
||
rclone sync ":s3:foxhunt-training-data/futures-baseline" /data/futures-baseline \$RCLONE_FLAGS --stats-one-line -v
|
||
echo "Syncing MBP-10 data..."
|
||
rclone sync ":s3:foxhunt-training-data/futures-baseline-mbp10" /data/futures-baseline-mbp10 \$RCLONE_FLAGS --stats-one-line -v
|
||
echo "Syncing trades data..."
|
||
rclone sync ":s3:foxhunt-training-data/futures-baseline-trades" /data/futures-baseline-trades \$RCLONE_FLAGS --stats-one-line -v
|
||
echo "Data sync complete:"
|
||
du -sh /data/futures-baseline/ /data/futures-baseline-mbp10/ /data/futures-baseline-trades/ 2>/dev/null || true
|
||
env:
|
||
- name: MINIO_ACCESS_KEY
|
||
valueFrom:
|
||
secretKeyRef:
|
||
name: minio-credentials
|
||
key: access-key
|
||
- name: MINIO_SECRET_KEY
|
||
valueFrom:
|
||
secretKeyRef:
|
||
name: minio-credentials
|
||
key: secret-key
|
||
volumeMounts:
|
||
- name: training-data
|
||
mountPath: /data
|
||
resources:
|
||
requests:
|
||
cpu: "1"
|
||
memory: 512Mi
|
||
limits:
|
||
cpu: "2"
|
||
memory: 1Gi
|
||
containers:
|
||
- name: evaluate
|
||
image: ${IMAGE}
|
||
command: ["/binaries/${EVAL_BINARY}"]
|
||
args:
|
||
- "--model=both"
|
||
- "--data-dir=/data/futures-baseline"
|
||
- "--models-dir=/output/models"
|
||
env:
|
||
- name: RUST_LOG
|
||
value: info
|
||
- name: SQLX_OFFLINE
|
||
value: "true"
|
||
resources:
|
||
requests:
|
||
nvidia.com/gpu: "1"
|
||
cpu: "4"
|
||
memory: "16Gi"
|
||
limits:
|
||
nvidia.com/gpu: "1"
|
||
cpu: "8"
|
||
memory: "32Gi"
|
||
volumeMounts:
|
||
- name: training-data
|
||
mountPath: /data
|
||
readOnly: true
|
||
- name: output
|
||
mountPath: /output
|
||
- name: binaries
|
||
mountPath: /binaries
|
||
readOnly: true
|
||
volumes:
|
||
- name: training-data
|
||
persistentVolumeClaim:
|
||
claimName: training-data-pvc
|
||
- name: output
|
||
emptyDir:
|
||
sizeLimit: 2Gi
|
||
- name: binaries
|
||
emptyDir:
|
||
sizeLimit: 500Mi
|
||
EOF
|
||
}
|
||
|
||
# --- Submit a single job and collect its name ------------------------------
|
||
submitted_jobs=()
|
||
|
||
submit_job() {
|
||
local model="$1"
|
||
local job_name="train-${model}-${TIMESTAMP}"
|
||
|
||
echo "--- Submitting job: ${job_name} (model=${model}, preset=${PRESET})"
|
||
generate_job_manifest "$model" | kubectl apply -f -
|
||
submitted_jobs+=("$job_name")
|
||
}
|
||
|
||
# --- Main dispatch ---------------------------------------------------------
|
||
echo "======================================================================"
|
||
echo " Foxhunt Training Orchestrator"
|
||
echo " Preset : ${PRESET}"
|
||
if [[ "$PRESET" == "ci-train" ]]; then
|
||
echo " Commit : ${COMMIT:0:12}"
|
||
echo " After : ${TRAIN_PRESET} --model ${MODEL}"
|
||
echo " Poll : every ${POLL_INTERVAL}s"
|
||
fi
|
||
echo " Symbol : ${SYMBOL}"
|
||
echo " Image : ${IMAGE}"
|
||
echo " NS : ${NAMESPACE}"
|
||
echo " Run ID : ${RUN_ID}"
|
||
echo " MinIO : minio://${S3_BUCKET}/runs/${RUN_ID}/"
|
||
echo "======================================================================"
|
||
echo ""
|
||
|
||
case "$PRESET" in
|
||
quick-test|single-model|hyperopt)
|
||
submit_job "$MODEL"
|
||
;;
|
||
full-ensemble)
|
||
for m in "${ALL_MODELS[@]}"; do
|
||
submit_job "$m"
|
||
echo " Waiting for ${m} to complete before next model..."
|
||
kubectl -n "${NAMESPACE}" wait --for=condition=complete "job/train-${m}-${TIMESTAMP}" --timeout="${TIMEOUT}s" 2>/dev/null || true
|
||
done
|
||
;;
|
||
evaluate)
|
||
echo "--- Submitting evaluation job for run ${RUN_ID}"
|
||
generate_eval_manifest | kubectl apply -f -
|
||
submitted_jobs+=("eval-${RUN_ID}-${TIMESTAMP}")
|
||
;;
|
||
ci-train)
|
||
echo "--- CI-Train: monitoring CI for commit ${COMMIT:0:8}, then auto-submitting ${TRAIN_PRESET} job"
|
||
echo ""
|
||
if wait_for_ci "$COMMIT"; then
|
||
echo "======================================================================"
|
||
echo " CI passed — submitting ${TRAIN_PRESET} training for model=${MODEL}"
|
||
echo "======================================================================"
|
||
echo ""
|
||
# Switch to the training preset for job generation
|
||
PRESET="$TRAIN_PRESET"
|
||
# Apply hyperopt timeout if needed
|
||
if [[ "$TRAIN_PRESET" == "hyperopt" && "$TIMEOUT" -eq 3600 ]]; then
|
||
TIMEOUT=21600
|
||
fi
|
||
submit_job "$MODEL"
|
||
else
|
||
echo "======================================================================"
|
||
echo " CI failed — NOT submitting training job"
|
||
echo "======================================================================"
|
||
exit 1
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
echo ""
|
||
echo "======================================================================"
|
||
echo " All jobs submitted."
|
||
echo "======================================================================"
|
||
echo ""
|
||
echo "Monitor with:"
|
||
echo ""
|
||
for jn in "${submitted_jobs[@]}"; do
|
||
echo " kubectl -n ${NAMESPACE} get job ${jn}"
|
||
echo " kubectl -n ${NAMESPACE} logs job/${jn} -f"
|
||
echo ""
|
||
done
|
||
echo " kubectl -n ${NAMESPACE} get jobs -l foxhunt/preset=${PRESET}"
|
||
echo " kubectl -n ${NAMESPACE} get pods -l foxhunt/job-type=training --watch"
|