← all scripts

demo/live_demo.sh

537 of 537 statements covered (100.00%).

coveredmissednever traced by Bash (not counted)A line ending in … continues the statement above it and shares its fate.

18#!/usr/bin/env bash
2# ─────────────────────────────────────────────────────────────────────────────
3# GCO Live Feature Demonstration
4# ─────────────────────────────────────────────────────────────────────────────
5# This script is designed to run in a visible terminal during a live
6# presentation. It walks through GCO's core capabilities automatically,
7# with narrated output and pauses between sections so the audience can
8# follow along.
9#
10# The script reads cdk.json to detect which optional features (schedulers,
11# FSx, Valkey) are enabled and only demos what's actually deployed.
12#
13# Usage:
14# bash demo/live_demo.sh # Standard run
15# GCO_DEMO_REGION=us-west-2 bash demo/live_demo.sh # Override region
16# GCO_DEMO_FAST=1 bash demo/live_demo.sh # Shorter pauses
17# SKIP_COSTS=1 bash demo/live_demo.sh # Skip cost section
18# SKIP_SCHEDULERS=1 bash demo/live_demo.sh # Skip scheduler demos
19#
20# See demo/LIVE_DEMO.md for full documentation and maintenance guide.
21# ─────────────────────────────────────────────────────────────────────────────
22
23# Exit immediately on errors, treat unset variables as errors, and propagate
24# failures through pipes (e.g., "cmd | sed" fails if cmd fails).
2537set -euo pipefail
26
27# ── Load Shared Library ──────────────────────────────────────────────────────
28# All helper functions (colors, display, feature detection, ARN helpers) live
29# in lib_demo.sh so they can be shared with record_demo.sh and tested
30# directly by BATS without duplication.
31
32148SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
33# shellcheck source=demo/lib_demo.sh
3437source "${SCRIPT_DIR}/lib_demo.sh"
35
36# Initialize colors and pause durations.
3737setup_colors
3837setup_pauses
39
40# Longer wait for pods to pull images and complete (not affected by FAST mode
41# because pods need real time regardless of presentation speed).
4237WAIT_FOR_POD="${GCO_DEMO_FAST:+15}"
4337WAIT_FOR_POD="${WAIT_FOR_POD:-30}"
44
45# Once an inference deployment is accepted, keep a child-shell EXIT fallback
46# armed until the normal delete succeeds. This prevents a later demo failure or
47# interruption from silently leaving the endpoint running.
4837INFERENCE_CLEANUP_PENDING=0
49cleanup_demo_inference_on_exit() {
508 local exit_code="$1"
518 trap - EXIT
528 if [ "${INFERENCE_CLEANUP_PENDING:-0}" = "1" ] && \
53 [ -n "${INFERENCE_NAME:-}" ]; then
549 cleanup_inference_endpoint "$INFERENCE_NAME" || true
55 fi
568 exit "$exit_code"
57}
58
59# ── Preflight Validation ─────────────────────────────────────────────────────
60# Before the demo starts, we automatically check every prerequisite.
61# This prevents embarrassing failures mid-presentation. Each check prints
62# a pass/fail/warn line, and at the end we show a summary. If anything
63# critical failed, the presenter can bail out or type "force" to continue.
64
6537CDK_JSON="cdk.json"
66
67# Counters for the summary line at the end of preflight.
6837PREFLIGHT_PASS=0
6937PREFLIGHT_FAIL=0
7037PREFLIGHT_WARN=0
71
72# preflight_pass: Green checkmark — this prerequisite is satisfied.
73preflight_pass() {
74255 echo " ${GREEN}${BOLD}✓${RESET} $1"
75255 PREFLIGHT_PASS=$((PREFLIGHT_PASS + 1))
76}
77
78# preflight_fail: Red X — this prerequisite is missing. Second arg is the fix.
79preflight_fail() {
809 echo " ${RED}${BOLD}✗${RESET} $1"
819 echo " ${DIM}Fix: $2${RESET}"
829 PREFLIGHT_FAIL=$((PREFLIGHT_FAIL + 1))
83}
84
85# preflight_warn: Yellow bang — not ideal but won't block the demo.
86preflight_warn() {
8743 echo " ${YELLOW}${BOLD}!${RESET} $1"
8843 echo " ${DIM}$2${RESET}"
8943 PREFLIGHT_WARN=$((PREFLIGHT_WARN + 1))
90}
91
92# Clear the screen for a clean start.
9337clear
94
9537banner "GCO — Global Capacity Orchestrator on AWS"
96
9737echo " ${BOLD}Preflight Check${RESET}"
9837narrate "Validating environment before starting the demo..."
9937spacer
100
101# ── Check 1: cdk.json exists ────────────────────────────────────────────────
102# cdk.json is the project config file. If it's missing, we're not in the
103# repo root and nothing else will work.
10437if [ -f "$CDK_JSON" ]; then
10536 preflight_pass "cdk.json found"
106else
1071 preflight_fail "cdk.json not found" "Run this script from the repo root"
1081 echo ""
1091 echo " ${RED}Cannot continue without cdk.json. Exiting.${RESET}"
1101 exit 1
111fi
112
113# ── Check 2: jq installed ───────────────────────────────────────────────────
114# jq is used to parse cdk.json and detect which features are enabled.
11536if command -v jq &>/dev/null; then
11670 preflight_pass "jq installed ($(jq --version 2>&1))"
117else
1181 preflight_fail "jq not installed" "brew install jq (macOS) or apt install jq (Linux)"
1191 echo ""
1201 echo " ${RED}Cannot continue without jq. Exiting.${RESET}"
1211 exit 1
122fi
123
124# ── Check 3: GCO CLI installed ──────────────────────────────────────────────
125# The gco CLI is the main interface we demo. Without it, there's no demo.
12635if command -v gco &>/dev/null; then
127102 GCO_VER=$(gco --version 2>&1 | head -1)
12834 preflight_pass "GCO CLI installed ($GCO_VER)"
129else
1301 preflight_fail "GCO CLI not installed" "pipx install -e . (from repo root)"
1311 echo ""
1321 echo " ${RED}Cannot continue without gco CLI. Exiting.${RESET}"
1331 exit 1
134fi
135
136# ── Check 4: kubectl installed ──────────────────────────────────────────────
137# kubectl is needed to watch pods, get logs, and interact with the cluster
138# during the scheduler and storage demos.
13934if command -v kubectl &>/dev/null; then
14099 KUBECTL_VER=$(kubectl version --client -o json 2>/dev/null \
141 | jq -r '.clientVersion.gitVersion // "unknown"' 2>/dev/null || echo "unknown")
14233 preflight_pass "kubectl installed ($KUBECTL_VER)"
143else
1441 preflight_fail "kubectl not installed" "https://kubernetes.io/docs/tasks/tools/"
1451 echo ""
1461 echo " ${RED}Cannot continue without kubectl. Exiting.${RESET}"
1471 exit 1
148fi
149
150# ── Read config values needed for remaining checks ──────────────────────────
151# Uses library functions for region and endpoint detection.
15233detect_region "$CDK_JSON"
15333detect_endpoint_access "$CDK_JSON"
154
155# ── Check 5: Infrastructure deployed ────────────────────────────────────────
156# Verify that GCO stacks have been deployed. We check the output of
157# "gco stacks list" for known stack name patterns.
15866STACK_CHECK=$(gco stacks list 2>&1 || true)
15966if echo "$STACK_CHECK" | grep -qi \
160 "gco-.*east\|gco-.*west\|gco-.*eu\|deployed\|CREATE_COMPLETE\|UPDATE_COMPLETE"; then
16131 preflight_pass "Infrastructure deployed (stacks detected)"
162else
1632 preflight_fail "No deployed stacks detected" "gco stacks deploy-all -y"
164fi
165
166# ── Check 6: EKS endpoint access mode ───────────────────────────────────────
167# For the demo, we need kubectl to reach the EKS API server from the
168# presenter's laptop. This requires PUBLIC or PUBLIC_AND_PRIVATE mode.
169# PRIVATE mode means kubectl only works from inside the VPC.
17066if [ "$ENDPOINT_ACCESS" = "PUBLIC_AND_PRIVATE" ] || [ "$ENDPOINT_ACCESS" = "PUBLIC" ]; then
17132 preflight_pass "EKS endpoint access: $ENDPOINT_ACCESS"
172else
1731 preflight_warn "EKS endpoint access is $ENDPOINT_ACCESS" \
174 "kubectl may not work from this machine. Set to PUBLIC_AND_PRIVATE in cdk.json and redeploy."
175fi
176
177# ── Check 7: kubectl can reach the cluster ──────────────────────────────────
178# Actually try to talk to the cluster. If it fails, we attempt to auto-
179# configure access using the setup script.
18070KUBECTL_TEST=$(kubectl get nodes --request-timeout=5s 2>&1 || true)
18166if echo "$KUBECTL_TEST" | grep -qiE "NAME|Ready|STATUS"; then
182 # Cluster responded and has nodes
18384 NODE_COUNT=$(echo "$KUBECTL_TEST" | grep -c "Ready" 2>/dev/null || echo "0")
18428 preflight_pass "kubectl connected to cluster ($NODE_COUNT node(s) ready)"
18510elif echo "$KUBECTL_TEST" | grep -qi "no resources found"; then
186 # Cluster responded but has zero nodes (normal for scale-to-zero)
1871 preflight_pass "kubectl connected to cluster (0 nodes — will scale on demand)"
188else
189 # Guarded recordings never auto-configure cluster access after the recorder
190 # has exported and validated its private kubeconfig snapshot. Repository CLI
191 # calls may refresh that disposable copy; the operator's kubeconfig remains
192 # untouched. Normal interactive demos retain the convenience auto-setup path.
1934 if [ "${GCO_DEMO_GUARDED_RECORDING:-}" = "1" ]; then
1941 preflight_fail "kubectl cannot reach the pre-authorized cluster" \
195 "Restore the validated context before recording; auto-setup is disabled"
1963 elif [ -f "./scripts/setup-cluster-access.sh" ]; then
1972 narrate " Attempting to configure cluster access..."
1982 bash ./scripts/setup-cluster-access.sh "gco-$REGION" "$REGION" 2>&1 || true
1995 KUBECTL_RETRY=$(kubectl get nodes --request-timeout=5s 2>&1 || true)
2004 if echo "$KUBECTL_RETRY" | grep -qiE "NAME|Ready|no resources found"; then
2011 preflight_pass "kubectl connected (auto-configured via setup-cluster-access.sh)"
202 else
2031 preflight_fail "kubectl cannot reach the cluster" \
204 "./scripts/setup-cluster-access.sh gco-$REGION $REGION"
205 fi
206 else
2071 preflight_fail "kubectl cannot reach the cluster" \
208 "./scripts/setup-cluster-access.sh gco-$REGION $REGION"
209 fi
210fi
211
212# ── Check 8: Terminal width ─────────────────────────────────────────────────
213# The demo output looks best at 120+ columns. Narrower terminals cause
214# wrapping that makes the output harder to read for the audience.
215# Check COLUMNS env var first (set by asciinema), then fall back to tput.
21633TERM_COLS="${COLUMNS:-$(tput cols 2>/dev/null || echo "80")}"
21733if [ "$TERM_COLS" -ge 120 ]; then
21823 preflight_pass "Terminal width: ${TERM_COLS} columns"
21910elif [ "$TERM_COLS" -ge 90 ]; then
2209 preflight_warn "Terminal width: ${TERM_COLS} columns (120+ recommended)" \
221 "Widen your terminal for best presentation appearance."
222else
2231 preflight_warn "Terminal width: ${TERM_COLS} columns (120+ recommended)" \
224 "Output may wrap and look messy. Widen your terminal window."
225fi
226
227# ── Check 9: Color support ──────────────────────────────────────────────────
228# Verify the terminal supports colors. Without colors the demo still works
229# but looks much less polished.
23034if [ -t 1 ] && [ "${TERM:-dumb}" != "dumb" ]; then
2311 preflight_pass "Terminal supports colors"
232else
23332 preflight_warn "Terminal may not support colors" \
234 "Try: TERM=xterm-256color bash demo/live_demo.sh"
235fi
236
237# ── Read feature flags from cdk.json ────────────────────────────────────────
238# Uses detect_features() from lib_demo.sh to set the global flag variables.
23933detect_features "$CDK_JSON"
24033detect_region "$CDK_JSON"
241
242# ── Preflight Summary ───────────────────────────────────────────────────────
243# Show the pass/fail/warn totals. If anything critical failed, give the
244# presenter a chance to bail out or force-continue.
245
24633spacer
24733echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}"
24833echo " ${BOLD}Results:${RESET} ${GREEN}${PREFLIGHT_PASS} passed${RESET} ${RED}${PREFLIGHT_FAIL} failed${RESET} ${YELLOW}${PREFLIGHT_WARN} warnings${RESET}"
24933echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}"
250
25133if [ "$PREFLIGHT_FAIL" -gt 0 ]; then
2525 spacer
2535 echo " ${RED}${BOLD}$PREFLIGHT_FAIL check(s) failed. Fix the issues above before demoing.${RESET}"
2545 if [ "${GCO_DEMO_GUARDED_RECORDING:-}" = "1" ]; then
2551 echo " ${RED}Guarded recording mode never force-continues preflight failures.${RESET}"
2561 exit 1
257 fi
2584 spacer
2594 echo " ${DIM}Press Enter to exit, or type 'force' to continue anyway:${RESET}"
2604 if [ "${GCO_DEMO_NONINTERACTIVE:-}" = "1" ]; then
2612 force_input="force"
262 else
2632 read -r force_input
264 fi
2654 if [ "$force_input" != "force" ]; then
2661 exit 1
267 fi
2683 warn "Continuing despite failures — some demo sections may break."
269fi
270
271# ── Feature Summary ──────────────────────────────────────────────────────────
272# Show the audience which features are enabled so they know what to expect.
273
27431spacer
27531echo " ${BOLD}One API. Every Accelerator. Any Region.${RESET}"
27631spacer
27731narrate "This live demonstration walks through GCO's core capabilities."
27831narrate "The script auto-detects which features are enabled in your deployment."
27931spacer
28031echo " ${BOLD}Region:${RESET} $REGION"
28162echo " ${BOLD}Volcano:${RESET} $(feature_status "$VOLCANO_ENABLED")"
28262echo " ${BOLD}Kueue:${RESET} $(feature_status "$KUEUE_ENABLED")"
28362echo " ${BOLD}YuniKorn:${RESET} $(feature_status "$YUNIKORN_ENABLED")"
28462echo " ${BOLD}Slurm:${RESET} $(feature_status "$SLURM_ENABLED")"
28562echo " ${BOLD}FSx Lustre:${RESET} $(feature_status "$FSX_ENABLED")"
28662echo " ${BOLD}Valkey:${RESET} $(feature_status "$VALKEY_ENABLED")"
28762echo " ${BOLD}Aurora pgvector:${RESET} $(feature_status "$AURORA_PGVECTOR_ENABLED")"
28862echo " ${BOLD}Vector store:${RESET} $(feature_status "$VECTOR_STORE_ENABLED")"
28931spacer
290
29131pause_for_audience
292
293# ── Pre-Demo Cleanup ─────────────────────────────────────────────────────────
294# Delete leftover jobs from previous demo runs and wait for their pods to
295# disappear. Volcano's webhook rejects updates to existing jobs, so we need
296# a clean slate. Stale `Terminating` pods also count against the GPU/memory
297# resource quota until they're fully gone — skipping the wait makes the
298# next Kueue or Volcano submit fail with a quota error. Runs silently.
29931narrate "Cleaning up any leftover jobs from previous runs..."
30031if [ "${GCO_DEMO_GUARDED_RECORDING:-}" = "1" ]; then
30130 recording_project=$(jq -r '.context.project_name // "gco"' "$CDK_JSON")
30215 detect_region "$CDK_JSON"
30315 verify_recording_kube_context \
304 "${recording_project}-${REGION}" "$REGION"
305fi
30630kubectl delete jobs --all -n gco-jobs --ignore-not-found=true >/dev/null 2>&1 || true
30730kubectl delete vcjob --all -n gco-jobs --ignore-not-found=true >/dev/null 2>&1 || true
30831gco inference delete demo-llm -y >/dev/null 2>&1 || true
309
310# Wait up to 30s for the job-owned pods to actually disappear. Without this,
311# a fresh submit can hit "forbidden: exceeded quota" because the old pod's
312# GPU/CPU/memory requests are still reserved during the termination window.
313#
314# Note on the pipeline shape: ``grep -c`` still prints ``0`` to stdout when
315# there are no matches, but exits 1. Under ``set -euo pipefail`` that would
316# kill the script silently right after "Cleaning up any leftover jobs".
317# Running the substitution inside ``|| true`` neutralizes the exit code
318# without doubling-up the output the way ``|| echo 0`` would.
31961for _ in $(seq 1 30); do
320123 LEFTOVER=$(
321 { kubectl get pods -n gco-jobs --no-headers 2>/dev/null \
322 | grep -cEv '^(gco-|slinky-)'; } || true
323 )
32431 if [ "${LEFTOVER:-0}" -eq 0 ]; then
32530 break
326 fi
3271 sleep 1
328done
32930success "Cleanup complete."
33030spacer
331
332# ── Pre-Deploy Inference (background) ────────────────────────────────────────
333# Deploy the inference endpoint now so the GPU node provisions while we demo
334# costs, capacity, schedulers, and storage. By the time we reach the inference
335# section, the model should be loaded and ready to serve.
33630if [ "${SKIP_INFERENCE:-}" != "1" ]; then
33728 INFERENCE_NAME="demo-llm"
338 # Wait for any leftover pods from previous runs to fully terminate
33928 narrate "Waiting for previous inference pods to terminate..."
340190 for _ in $(seq 1 20); do
341324 OLD_PODS=$(kubectl get pods -n gco-inference -l app="$INFERENCE_NAME" --no-headers 2>/dev/null || true)
342162 if [ -z "$OLD_PODS" ]; then
34321 break
344 fi
345 # Force-delete stuck Terminating pods after a few attempts
346282 if echo "$OLD_PODS" | grep -q "Terminating"; then
3471 kubectl delete pods -n gco-inference -l app="$INFERENCE_NAME" --force --grace-period=0 >/dev/null 2>&1 || true
348 fi
349141 sleep 3
350 done
35128 narrate "Pre-deploying inference endpoint (GPU will provision in background)..."
352 # Retry deploy in case the previous endpoint hasn't been fully cleaned up yet.
35328 DEPLOY_OUTPUT=""
35428 INFERENCE_DEPLOYED=false
35560 for deploy_attempt in $(seq 1 5); do
35664 if DEPLOY_OUTPUT=$(gco inference deploy "$INFERENCE_NAME" -i vllm/vllm-openai:v0.29.0 \
357 --gpu-count 1 --replicas 1 -r "$REGION" \
358 --extra-args '--model' --extra-args 'facebook/opt-125m' 2>&1) && \
35954 echo "$DEPLOY_OUTPUT" | grep -qi "registered\|success"; then
36027 INFERENCE_DEPLOYED=true
36127 break
362 fi
3635 if [ "$deploy_attempt" -lt 5 ]; then
3644 sleep 5
365 fi
366 done
36728 if [ "$INFERENCE_DEPLOYED" != "true" ]; then
3681 if [ -n "$DEPLOY_OUTPUT" ]; then
3691 printf ' %s\n' "${DEPLOY_OUTPUT//$'\n'/$'\n '}"
3701 fi
3711 warn "Inference deployment was not accepted after 5 attempts."
3721 cleanup_inference_endpoint "$INFERENCE_NAME" || true
3731 exit 1
3741 fi
37527 INFERENCE_CLEANUP_PENDING=1
37627 trap 'cleanup_demo_inference_on_exit "$?"' EXIT
37727 success "Inference endpoint queued for deployment."
37827 spacer
379fi
380
381# ═════════════════════════════════════════════════════════════════════════════
382# SECTION: Fleet Overview
383# ═════════════════════════════════════════════════════════════════════════════
384# One aggregate document replaces four separate cost/status calls in the
385# recording: stack state, queue/jobs, capacity, inference, policy agreement,
386# and the optional 30-day Cost Explorer view.
387
38829if [ "${SKIP_COSTS:-}" != "1" ]; then
389
39054SECTION=$((SECTION + 1)); section_header "$SECTION" "FLEET OVERVIEW — Status, Cost, and Policy" "$GREEN"
391
39227narrate "Start with one fleet-wide answer: what is deployed, what is queued,"
39327narrate "where capacity exists, whether policy agrees, and what it costs."
39427spacer
395
39627highlight "Aggregate status across every configured region"
39727run_cmd "gco status --with-costs --with-policy"
39827sleep "$PAUSE_SHORT"
399
40027success "One command joins the control plane without hiding unavailable sections."
40127narrate "The base fleet document is also available through the MCP server."
40227narrate "Policy comparison is CLI-only; the CLI can also emit strict JSON."
403
40427pause_for_audience
405
406fi # SKIP_COSTS
407
408# ═════════════════════════════════════════════════════════════════════════════
409# SECTION: Capacity Discovery
410# ═════════════════════════════════════════════════════════════════════════════
411# Shows how GCO finds GPU capacity across regions and routes jobs to where
412# resources are actually available.
413
41429if [ "${SKIP_CAPACITY:-}" != "1" ]; then
415
41654SECTION=$((SECTION + 1)); section_header "$SECTION" "CAPACITY DISCOVERY — Find GPUs Across Regions" "$GREEN"
417
41827narrate "GPU availability varies by region and changes constantly."
41927narrate "GCO checks Spot Placement Scores and instance availability"
42027narrate "across all configured regions to find where GPUs are right now."
42127spacer
422
42327highlight "Check GPU availability in a specific region"
42427run_cmd "gco capacity check --instance-type g4dn.xlarge --region $REGION" || true
42527sleep "$PAUSE_SHORT"
426
42727highlight "Find the best region for GPU workloads"
42827run_cmd "gco capacity recommend-region --gpu" || true
42927sleep "$PAUSE_SHORT"
430
43127highlight "Submit a job with automatic region selection"
43227narrate "The CLI analyzes capacity across all regions, picks the best one,"
43327narrate "and places the job on that region's SQS queue automatically."
43427run_cmd "gco jobs submit-sqs examples/simple-job.yaml --auto-region" || true
43527sleep "$PAUSE_SHORT"
436
43727highlight "Check the SQS queue status across all regions"
43827run_cmd "gco jobs queue-status --all-regions" || true
43927sleep "$PAUSE_SHORT"
440
44127success "Capacity-aware job placement without manual region selection."
442
44327pause_for_audience
444
445fi # SKIP_CAPACITY
446
447# ═════════════════════════════════════════════════════════════════════════════
448# SECTION: Schedulers
449# ═════════════════════════════════════════════════════════════════════════════
450# GCO supports multiple Kubernetes schedulers simultaneously. Each one is
451# opt-in via cdk.json. We only demo the ones that are actually enabled.
452# This section is skipped entirely if SKIP_SCHEDULERS=1.
453
45429if [ "${SKIP_SCHEDULERS:-}" != "1" ]; then
455
456# Track how many schedulers we demo for the summary at the end.
45727SCHEDULER_COUNT=0
458
459# ── Volcano ──────────────────────────────────────────────────────────────────
460# Volcano is a CNCF batch scheduler for Kubernetes. Its main feature is
461# "gang scheduling" — all pods in a distributed training job must be
462# schedulable at the same time, or none of them start. This prevents
463# deadlocks in multi-node training.
464
46527if [ "$VOLCANO_ENABLED" = "true" ]; then
466
46718SECTION=$((SECTION + 1)); section_header "$SECTION" "VOLCANO — Gang Scheduling for Distributed Training" "$MAGENTA"
468
4699narrate "Volcano is a Kubernetes-native batch scheduler built for AI/ML."
4709narrate "Its killer feature: gang scheduling — all pods in a distributed"
4719narrate "training job start together, or none of them start at all."
4729narrate "This prevents deadlocks where half the workers are waiting forever."
4739spacer
474
4759highlight "Submitting a Volcano gang-scheduled job (1 master + 2 workers)"
47610run_cmd "gco jobs submit-direct examples/volcano-gang-job.yaml -r $REGION -n gco-jobs" || true
4779sleep "$PAUSE_SHORT"
478
4799narrate "Volcano ensures all 3 pods are co-scheduled atomically."
4809narrate "Let's watch them come up together..."
4819spacer
482
4839highlight "Checking job status"
4849countdown "Waiting for pods to schedule" "$WAIT_FOR_POD"
4859run_cmd "kubectl get pods -n gco-jobs -l volcano.sh/job-name=distributed-training --no-headers 2>/dev/null || echo ' (pods not yet visible — node provisioning in progress)'"
486
4879highlight "Volcano job status"
4889run_cmd "kubectl get vcjob -n gco-jobs --no-headers 2>/dev/null || echo ' (checking Volcano job status...)'"
489
4909success "Gang scheduling ensures distributed training jobs don't deadlock."
491# Release resource-quota reservations held by this job's pods so the next
492# scheduler section doesn't hit "exceeded quota" on submit.
4939kubectl delete vcjob distributed-training -n gco-jobs --ignore-not-found=true >/dev/null 2>&1 || true
4949SCHEDULER_COUNT=$((SCHEDULER_COUNT + 1))
495
4969pause_for_audience
497
498fi # VOLCANO
499
500# ── Kueue ────────────────────────────────────────────────────────────────────
501# Kueue is the Kubernetes-native job queueing system (SIG Scheduling).
502# It manages resource quotas per team/namespace and holds jobs in a queue
503# until the cluster has enough resources to run them.
504
50527if [ "$KUEUE_ENABLED" = "true" ]; then
506
50718SECTION=$((SECTION + 1)); section_header "$SECTION" "KUEUE — Quota-Based Job Queueing" "$MAGENTA"
508
5099narrate "Kueue is the Kubernetes-native job queueing system."
5109narrate "It manages resource quotas, fair-sharing between teams, and"
5119narrate "holds jobs in a queue until cluster resources are available."
5129narrate "Think of it as a resource-aware admission controller for batch jobs."
5139spacer
514
5159highlight "Submitting a Kueue-managed job"
51610run_cmd "gco jobs submit-direct examples/kueue-job.yaml -r $REGION -n gco-jobs" || true
5179sleep "$PAUSE_SHORT"
518
5199highlight "Checking Kueue queue status"
5209run_cmd "kubectl get clusterqueue --no-headers 2>/dev/null || echo ' (ClusterQueue not yet created — will be created by the manifest)'"
5219run_cmd "kubectl get localqueue -n gco-jobs --no-headers 2>/dev/null || echo ' (LocalQueue not yet created)'"
522
5239countdown "Waiting for workload admission" "$PAUSE_SHORT"
524
5259highlight "Kueue workloads (jobs waiting or admitted)"
5269run_cmd "kubectl get workloads -n gco-jobs --no-headers 2>/dev/null || echo ' (no workloads yet)'"
527
5289success "Kueue prevents resource overcommit and enforces team quotas."
529# Release resource-quota reservations from these jobs before the next section.
5309kubectl delete job kueue-sample-job kueue-gpu-job -n gco-jobs --ignore-not-found=true >/dev/null 2>&1 || true
5319SCHEDULER_COUNT=$((SCHEDULER_COUNT + 1))
532
5339pause_for_audience
534
535fi # KUEUE
536
537# ── YuniKorn ─────────────────────────────────────────────────────────────────
538# Apache YuniKorn provides hierarchical queues and fair-sharing for
539# multi-tenant clusters. Teams get guaranteed resource shares with the
540# ability to borrow unused capacity from other teams.
541
54227if [ "$YUNIKORN_ENABLED" = "true" ]; then
543
54418SECTION=$((SECTION + 1)); section_header "$SECTION" "YUNIKORN — App-Aware Fair Scheduling" "$MAGENTA"
545
5469narrate "Apache YuniKorn brings hierarchical queues and fair-sharing"
5479narrate "to Kubernetes. It's designed for multi-tenant clusters where"
5489narrate "multiple teams compete for GPU resources."
5499narrate "YuniKorn also supports gang scheduling and preemption."
5509spacer
551
5529YUNIKORN_SUBMITTED=0
5539highlight "Submitting a YuniKorn-scheduled job"
5549if run_cmd "gco jobs submit-direct examples/yunikorn-job.yaml -r $REGION -n gco-jobs"; then
5557 YUNIKORN_SUBMITTED=1
556fi
5579sleep "$PAUSE_SHORT"
558
5599highlight "Checking YuniKorn pod scheduling"
5609countdown "Waiting for YuniKorn to place pods" "$PAUSE_SHORT"
5619run_cmd "kubectl get pods -n gco-jobs -l app=yunikorn-demo --no-headers 2>/dev/null || echo ' (pods scheduling...)'"
562
5639if ! report_feature_result "$YUNIKORN_SUBMITTED" "YuniKorn" \
564 "YuniKorn provides enterprise-grade multi-tenant scheduling."; then
5651 exit 1
566fi
567# Release resource-quota reservations from these jobs before the next section.
5688kubectl delete job yunikorn-sample-job yunikorn-gpu-job yunikorn-gang-job -n gco-jobs --ignore-not-found=true >/dev/null 2>&1 || true
5698SCHEDULER_COUNT=$((SCHEDULER_COUNT + 1))
570
5718pause_for_audience
572
573fi # YUNIKORN
574
575# ── Slurm ────────────────────────────────────────────────────────────────────
576# The Slinky Slurm Operator runs a full Slurm cluster inside Kubernetes.
577# This lets teams with existing HPC workflows (sbatch scripts, etc.) run
578# them on GCO without modification.
579
58026if [ "$SLURM_ENABLED" = "true" ]; then
581
58216SECTION=$((SECTION + 1)); section_header "$SECTION" "SLURM — HPC Batch Scheduling on Kubernetes" "$MAGENTA"
583
5848narrate "For teams coming from traditional HPC, GCO includes the Slinky"
5858narrate "Slurm Operator. It runs a full Slurm cluster inside Kubernetes,"
5868narrate "so existing sbatch scripts and workflows work unchanged."
5878narrate "This bridges the gap between HPC and cloud-native."
5888spacer
589
5908SLURM_SUBMITTED=0
5918highlight "Submitting a Slurm batch job via Kubernetes"
5928if run_cmd "gco jobs submit-direct examples/slurm-cluster-job.yaml -r $REGION -n gco-jobs"; then
5936 SLURM_SUBMITTED=1
594fi
5958sleep "$PAUSE_SHORT"
596
5978highlight "Checking Slurm job pod"
5988wait_for_job "slurm-test" "gco-jobs"
5998run_cmd "kubectl get pods -n gco-jobs -l job-name=slurm-test --no-headers 2>/dev/null || echo ' (Slurm job pod starting...)'"
600
6018highlight "Tailing Slurm job logs"
6028run_cmd "kubectl logs job/slurm-test -n gco-jobs --all-containers=true --tail=20 2>/dev/null || kubectl logs -n gco-jobs -l job-name=slurm-test --all-containers=true --tail=20 2>/dev/null || echo ' (no logs yet)'"
603
6048if ! report_feature_result "$SLURM_SUBMITTED" "Slurm" \
605 "Existing HPC workflows run on Kubernetes without modification."; then
6061 exit 1
607fi
608# Release resource-quota reservations so FSx / Valkey / EFS sections don't
609# hit quota errors. slurm-test itself goes away quickly; we also clean up
610# any Slurm-operator-owned workload pods that were spawned for this job.
6117kubectl delete job slurm-test -n gco-jobs --ignore-not-found=true >/dev/null 2>&1 || true
6127SCHEDULER_COUNT=$((SCHEDULER_COUNT + 1))
613
6147pause_for_audience
615
616fi # SLURM
617
618# ── Scheduler Summary ────────────────────────────────────────────────────────
619
62025if [ "$SCHEDULER_COUNT" -gt 0 ]; then
6217 spacer
6227 echo " ${GREEN}${BOLD}Demonstrated $SCHEDULER_COUNT scheduler(s) — plus KEDA running the SQS queue processor.${RESET}"
6237 narrate "GCO supports 5 schedulers simultaneously — pick the right"
6247 narrate "tool for each workload type, all on the same cluster."
6257 spacer
626fi
627
628fi # SKIP_SCHEDULERS
629
630# ═════════════════════════════════════════════════════════════════════════════
631# SECTION: FSx for Lustre
632# ═════════════════════════════════════════════════════════════════════════════
633# FSx for Lustre is a high-performance parallel file system. It provides
634# hundreds of GB/s of throughput with sub-millisecond latency — critical
635# for ML training on large datasets. This section only runs if FSx is
636# enabled in cdk.json.
637
63827if [ "$FSX_ENABLED" = "true" ]; then
639
64016SECTION=$((SECTION + 1)); section_header "$SECTION" "FSx FOR LUSTRE — High-Performance Scratch Storage" "$BLUE"
641
6428narrate "ML training on large datasets needs serious I/O throughput."
6438narrate "FSx for Lustre provides hundreds of GB/s of throughput with"
6448narrate "sub-millisecond latency — purpose-built for HPC and ML."
6458narrate "GCO provisions it automatically and mounts it into every cluster."
6468spacer
647
6488FSX_SUBMITTED=0
6498highlight "Submitting a job that exercises FSx Lustre storage"
6508if run_cmd "gco jobs submit-direct examples/fsx-lustre-job.yaml -r $REGION -n gco-jobs"; then
6516 FSX_SUBMITTED=1
652fi
653
6548highlight "Watching the FSx job"
6558wait_for_job "fsx-lustre-example" "gco-jobs"
6568run_cmd "kubectl get pods -n gco-jobs -l example=fsx-lustre --no-headers 2>/dev/null || echo ' (pod scheduling...)'"
657
6588spacer
6598narrate "The job writes 10 MB of simulated training data, saves a checkpoint,"
6608narrate "reads it all back, and reports throughput numbers."
6618spacer
662
6638highlight "Checking job logs for I/O performance"
6648run_cmd "kubectl logs job/fsx-lustre-example -n gco-jobs --all-containers=true --tail=30 2>/dev/null || kubectl logs -n gco-jobs -l example=fsx-lustre --all-containers=true --tail=30 2>/dev/null || echo ' (no logs yet)'"
665
6668if ! report_feature_result "$FSX_SUBMITTED" "FSx for Lustre" \
667 "FSx for Lustre: sub-millisecond latency, hundreds of GB/s throughput."; then
6681 exit 1
669fi
670# Release resource-quota reservations before Valkey/inference/EFS sections.
6717kubectl delete job fsx-lustre-example -n gco-jobs --ignore-not-found=true >/dev/null 2>&1 || true
6727narrate "Compare: EFS tops out around 10 GB/s. For large-scale training,"
6737narrate "FSx is the difference between hours and minutes."
674
6757pause_for_audience
676
677fi # FSX
678
679# ═════════════════════════════════════════════════════════════════════════════
680# SECTION: Valkey Cache
681# ═════════════════════════════════════════════════════════════════════════════
682# Valkey is the open-source successor to Redis. GCO deploys it as a
683# serverless cache in each region. Common uses: prompt caching (saves
684# 30-50% on inference costs), feature stores, and session state.
685
68626if [ "$VALKEY_ENABLED" = "true" ]; then
687
68816SECTION=$((SECTION + 1)); section_header "$SECTION" "VALKEY — Serverless In-Memory Cache" "$BLUE"
689
6908narrate "Valkey (the open-source Redis successor) runs as a serverless"
6918narrate "cache in each region. Use it for prompt caching, feature stores,"
6928narrate "session state, or any low-latency K/V access from your jobs."
6938narrate "The endpoint is injected automatically — no config needed in manifests."
6948spacer
695
6968VALKEY_SUBMITTED=0
6978highlight "Submitting a job that exercises the Valkey cache"
6988if run_cmd "gco jobs submit-direct examples/valkey-cache-job.yaml -r $REGION -n gco-jobs"; then
6996 VALKEY_SUBMITTED=1
700fi
701
7028highlight "Watching the Valkey job"
7038wait_for_job "valkey-cache-example" "gco-jobs"
7048run_cmd "kubectl get pods -n gco-jobs -l app=valkey-cache-example --no-headers 2>/dev/null || echo ' (pod scheduling...)'"
705
7068highlight "Valkey job output"
7078run_cmd "kubectl logs job/valkey-cache-example -n gco-jobs --all-containers=true --tail=20 2>/dev/null || kubectl logs -n gco-jobs -l app=valkey-cache-example --all-containers=true --tail=20 2>/dev/null || echo ' (no logs yet)'"
708
7098if ! report_feature_result "$VALKEY_SUBMITTED" "Valkey" \
710 "Serverless Valkey: zero management, auto-scaling, per-region."; then
7111 exit 1
712fi
713# Release resource-quota reservations before the inference/EFS sections.
7147kubectl delete job valkey-cache-example -n gco-jobs --ignore-not-found=true >/dev/null 2>&1 || true
7157narrate "Prompt caching alone can cut inference costs by 30-50%."
716
7177pause_for_audience
718
719fi # VALKEY
720
721# ═════════════════════════════════════════════════════════════════════════════
722# SECTION: Aurora pgvector
723# ═════════════════════════════════════════════════════════════════════════════
724# Aurora Serverless v2 with pgvector provides a fully managed vector database
725# for RAG, semantic search, and embedding storage. This section only runs if
726# Aurora pgvector is enabled in cdk.json.
727
72825if [ "$AURORA_PGVECTOR_ENABLED" = "true" ]; then
729
73012SECTION=$((SECTION + 1)); section_header "$SECTION" "AURORA PGVECTOR — Serverless Vector Database" "$BLUE"
731
7326narrate "For RAG, semantic search, and embedding storage, GCO can deploy"
7336narrate "Aurora Serverless v2 with pgvector in each region. It auto-scales"
7346narrate "capacity and requires no instance management."
7356narrate "Credentials are in Secrets Manager — pods discover them via ConfigMap."
7366spacer
737
7386AURORA_SUBMITTED=0
7396highlight "Submitting a job that exercises Aurora pgvector"
7406if run_cmd "gco jobs submit-direct examples/aurora-pgvector-job.yaml -r $REGION -n gco-jobs"; then
7414 AURORA_SUBMITTED=1
742fi
743
7446highlight "Watching the Aurora pgvector job"
7456wait_for_job "aurora-pgvector-example" "gco-jobs"
7466run_cmd "kubectl get pods -n gco-jobs -l app=aurora-pgvector-example --no-headers 2>/dev/null || echo ' (pod scheduling...)'"
747
7486highlight "Aurora pgvector job output"
7496run_cmd "kubectl logs job/aurora-pgvector-example -n gco-jobs --all-containers=true --tail=20 2>/dev/null || kubectl logs -n gco-jobs -l app=aurora-pgvector-example --all-containers=true --tail=20 2>/dev/null || echo ' (no logs yet)'"
750
7516if ! report_feature_result "$AURORA_SUBMITTED" "Aurora pgvector" \
752 "Serverless Aurora pgvector: vector search with zero management."; then
7531 exit 1
754fi
755# Release resource-quota reservations before the next section.
7565kubectl delete job aurora-pgvector-example -n gco-jobs --ignore-not-found=true >/dev/null 2>&1 || true
7575narrate "pgvector supports HNSW and IVFFlat indexes for fast similarity search."
758
7595pause_for_audience
760
761fi # AURORA_PGVECTOR
762
763# ═════════════════════════════════════════════════════════════════════════════
764# SECTION: Globally Replicated Vector Store
765# ═════════════════════════════════════════════════════════════════════════════
766# The complement to Aurora pgvector rather than a competitor: pgvector gives
767# SQL (joins, range predicates, transactions), this gives a DynamoDB global
768# table whose vector index and data replicate to every deployment region, so
769# every cluster reads its own local replica. Ingestion is S3-triggered — drop
770# a document on the cluster-shared bucket and a Lambda chunks, embeds, and
771# writes it. This section only runs if the vector store is enabled.
772
77324if [ "$VECTOR_STORE_ENABLED" = "true" ]; then
774
77510SECTION=$((SECTION + 1)); section_header "$SECTION" "VECTOR STORE — Globally Replicated Semantic Search" "$BLUE"
776
7775narrate "RAG workloads need their corpus close to the accelerators using it."
7785narrate "GCO can provision a DynamoDB global table with a vector index whose"
7795narrate "definition AND data replicate to every deployment region, so a job"
7805narrate "in any region searches a local replica — no cross-region hop."
7815spacer
782
7835highlight "Vector store, replicas, and index state"
7845run_cmd "gco vector status --output table" || true
785
7865spacer
7875narrate "Ingestion is just an upload: the S3 event invokes a Lambda that"
7885narrate "chunks each document, embeds it with Amazon Bedrock Titan, and"
7895narrate "writes the vectors. Let's seed it with GCO's own documentation."
7905spacer
791
7925VECTOR_INGESTED=0
7935highlight "Ingesting the checkout's docs/*.md as a demo corpus"
7945if run_cmd "gco vector ingest --demo --wait --output table"; then
7953 VECTOR_INGESTED=1
796fi
797
7985VECTOR_SEARCHED=0
7995if [ "$VECTOR_INGESTED" -eq 1 ]; then
8003 spacer
8013 narrate "Now a semantic query — not a keyword grep. The store returns the"
8023 narrate "passages closest in embedding space, with their similarity scores."
8033 spacer
804
8053 highlight "Semantic search: \"how does capacity history work?\""
8063 if run_cmd "gco vector search 'how does capacity history work?' --top-k 5 --output table"; then
8072 VECTOR_SEARCHED=1
808 fi
809
8103 highlight "The same query against the ${REGION} replica (local read)"
8114 run_cmd "gco vector search 'how does capacity history work?' --top-k 3 --region $REGION --output table" || true
812fi
813
814# The claim is "globally replicated semantic search", so both halves must hold:
815# a corpus that actually ingested, and a query that actually returned matches.
8165VECTOR_PROVEN=0
8178if [ "$VECTOR_INGESTED" -eq 1 ] && [ "$VECTOR_SEARCHED" -eq 1 ]; then
8182 VECTOR_PROVEN=1
819fi
8205if ! report_feature_result "$VECTOR_PROVEN" "Vector store" \
821 "Globally replicated vector search — ingest once, query in every region."; then
8222 exit 1
823fi
8243narrate "Complementary to Aurora pgvector, not a replacement: pgvector brings"
8253narrate "SQL joins and transactions, this brings managed global replication."
826
8273pause_for_audience
828
829fi # VECTOR_STORE
830
831# ═════════════════════════════════════════════════════════════════════════════
832# SECTION: EFS Shared Storage
833# ═════════════════════════════════════════════════════════════════════════════
834# EFS (Elastic File System) is always deployed — it's the default shared
835# storage for job outputs. This section always runs because EFS is a core
836# feature, not optional.
837
83844SECTION=$((SECTION + 1)); section_header "$SECTION" "EFS — Persistent Shared Storage" "$BLUE"
839
84022narrate "When a Kubernetes pod terminates, its local data vanishes."
84122narrate "GCO mounts Amazon EFS into every cluster so job outputs,"
84222narrate "model checkpoints, and training artifacts persist beyond pod lifetime."
84322narrate "You can download results even after the job is long gone."
84422spacer
845
84622highlight "Submitting a job that writes results to shared EFS storage"
84723run_cmd "gco jobs submit-direct examples/efs-output-job.yaml -r $REGION -n gco-jobs" || true
848
84922highlight "Watching the EFS job"
85022wait_for_job "efs-output-example" "gco-jobs"
85122run_cmd "kubectl get pods -n gco-jobs -l example=efs-output --no-headers 2>/dev/null || echo ' (pod scheduling...)'"
852
85322highlight "Job logs — results written to /outputs on EFS"
85422run_cmd "kubectl logs job/efs-output-example -n gco-jobs --all-containers=true --tail=15 2>/dev/null || kubectl logs -n gco-jobs -l example=efs-output --all-containers=true --tail=15 2>/dev/null || echo ' (no logs yet)'"
855
85622spacer
85722narrate "The pod is gone, but the data lives on. Let's prove it."
85822spacer
859
86022highlight "Listing files on shared EFS storage"
86122run_cmd "gco files ls -r $REGION" || true
862
86322highlight "Downloading results to local machine"
86422run_cmd "gco files download efs-output-example /tmp/gco-demo-results -r $REGION && cat /tmp/gco-demo-results/results.json"
865
86622success "Persistent storage that survives pod termination."
86722narrate "Critical for ML checkpoints, training artifacts, and audit trails."
868
86922pause_for_audience
870
871# ═════════════════════════════════════════════════════════════════════════════
872# SECTION: Inference Endpoint
873# ═════════════════════════════════════════════════════════════════════════════
874# The inference endpoint was deployed at the start of the demo (before costs)
875# so the GPU node could provision in the background. By now it should be
876# ready. We just need to wait for readiness, invoke it, and clean up.
877# Placed at the end to give the GPU node maximum time to provision.
878# Skippable with SKIP_INFERENCE=1 (useful if no GPU quota is available).
879
88022if [ "${SKIP_INFERENCE:-}" != "1" ]; then
881
88240SECTION=$((SECTION + 1)); section_header "$SECTION" "INFERENCE — Live LLM on GCO" "$CYAN"
883
88420narrate "GCO isn't just for batch jobs — it also manages multi-region"
88520narrate "inference endpoints. We deployed a vLLM endpoint at the start"
88620narrate "of this demo so the GPU could provision while we covered other"
88720narrate "features. Let's see if it's ready."
88820spacer
889
89020highlight "Checking if the inference endpoint is ready"
89120narrate "The endpoint was deployed with a single command at the start."
89220narrate "EKS Auto Mode provisioned a GPU node, pulled the vLLM image,"
89320narrate "and loaded the facebook/opt-125m model — all automatically."
894
895# Poll for the endpoint to become ready. Since we deployed it at the start,
896# it's had several minutes to provision. We still poll in case it's not
897# quite ready yet. Ignore Terminating pods from previous runs.
89820INFERENCE_READY=false
89991for attempt in $(seq 1 50); do
900214 POD_STATUS=$(kubectl get pods -n gco-inference -l app="$INFERENCE_NAME" --no-headers 2>/dev/null \
901 | grep -v "Terminating" || true)
902142 if echo "$POD_STATUS" | grep -q "1/1.*Running"; then
90319 INFERENCE_READY=true
90419 break
905 fi
906
90752 if [ -n "$POD_STATUS" ]; then
90851 echo " ${DIM}$POD_STATUS${RESET}"
909 else
9102 STATUS_OUTPUT=$(gco inference status "$INFERENCE_NAME" 2>&1 || true)
9113 echo "$STATUS_OUTPUT" | head -5 | sed 's/^/ /'
912 fi
913
91452 if [ "$attempt" -lt 50 ]; then
91551 countdown "Waiting for pod to be ready (attempt $attempt/50)" 15
916 fi
917done
918
91920INFERENCE_INVOKE_OK=0
92020if [ "$INFERENCE_READY" = "true" ]; then
92119 success "Inference pod is Kubernetes-ready."
92219 narrate "Kubernetes readiness is local; validating one real generation through the global route."
92319 if wait_for_inference_generation "$INFERENCE_NAME" 4 10; then
92418 success "End-to-end global inference route is ready."
92518 spacer
926
92718 highlight "Sending a prompt to the endpoint"
92818 narrate "The shared inference route is already registered on the internal ALB."
92918 narrate "Requests traverse API Gateway → Global Accelerator → ALB → authenticated proxy → vLLM."
93018 narrate "API Gateway validates SigV4; the private backend hop uses private-root TLS plus a request-bound HMAC."
93118 if run_cmd "gco inference invoke $INFERENCE_NAME -p 'The benefits of GPU orchestration for ML workloads are: 1)' --max-tokens 80"; then
93218 INFERENCE_INVOKE_OK=1
93318 sleep "$PAUSE_LONG"
93418 success "Live LLM response from a GPU that didn't exist minutes ago."
935 fi
936 else
9371 warn "The end-to-end inference route did not become ready after 4 attempts."
938 fi
939else
9401 warn "Endpoint did not become Kubernetes-ready within the bounded wait."
941fi
942
94320spacer
94420highlight "Cleaning up the inference endpoint"
94520narrate "This deletes the endpoint Deployment and internal Service. The GPU node"
94620narrate "scales back to zero automatically once the pod is gone."
94720INFERENCE_DELETE_OK=0
94820if run_cmd "gco inference delete $INFERENCE_NAME -y"; then
94919 INFERENCE_DELETE_OK=1
95019 INFERENCE_CLEANUP_PENDING=0
95119 trap - EXIT
952fi
953
95420if ! report_inference_lifecycle_result "$INFERENCE_INVOKE_OK" "$INFERENCE_DELETE_OK"; then
9553 exit 1
956fi
957
95817pause_for_audience
959
960fi # SKIP_INFERENCE
961
962# ═════════════════════════════════════════════════════════════════════════════
963# SECTION: Wrap-up
964# ═════════════════════════════════════════════════════════════════════════════
965# Summary of everything we covered, plus an optional cleanup step.
966
96719banner "Demo Complete"
968
96919echo " ${BOLD}What we covered:${RESET}"
97019spacer
97119echo " ${GREEN}✓${RESET} Fleet status, cost visibility, and policy agreement"
97219if [ "${SKIP_CAPACITY:-}" != "1" ]; then
97317 echo " ${GREEN}✓${RESET} Capacity discovery and auto-region job placement"
974fi
975
976# Only show scheduler items if we didn't skip that section.
97719if [ "${SKIP_SCHEDULERS:-}" != "1" ]; then
97817 if [ "$VOLCANO_ENABLED" = "true" ]; then
9792 echo " ${GREEN}✓${RESET} Volcano gang scheduling for distributed training"
980 fi
98117 if [ "$KUEUE_ENABLED" = "true" ]; then
9822 echo " ${GREEN}✓${RESET} Kueue quota-based job queueing"
983 fi
98417 if [ "$YUNIKORN_ENABLED" = "true" ]; then
9852 echo " ${GREEN}✓${RESET} YuniKorn app-aware fair scheduling"
986 fi
98717 if [ "$SLURM_ENABLED" = "true" ]; then
9882 echo " ${GREEN}✓${RESET} Slurm HPC batch scheduling on Kubernetes"
989 fi
990fi
991
99219if [ "$FSX_ENABLED" = "true" ]; then
9933 echo " ${GREEN}✓${RESET} FSx for Lustre high-performance storage"
994fi
99519if [ "$VALKEY_ENABLED" = "true" ]; then
9964 echo " ${GREEN}✓${RESET} Valkey serverless in-memory cache"
997fi
99819if [ "$AURORA_PGVECTOR_ENABLED" = "true" ]; then
9993 echo " ${GREEN}✓${RESET} Aurora pgvector serverless vector database"
1000fi
100119if [ "$VECTOR_STORE_ENABLED" = "true" ]; then
10023 echo " ${GREEN}✓${RESET} Globally replicated vector store with semantic search"
1003fi
100419echo " ${GREEN}✓${RESET} EFS persistent shared storage"
100519if [ "${SKIP_INFERENCE:-}" != "1" ]; then
100617 echo " ${GREEN}✓${RESET} Inference endpoint deploy, invoke, and teardown"
1007fi
1008
100919spacer
101019echo " ${BOLD}All of this runs on a single platform, deployed with one command:${RESET}"
101119echo " ${CYAN}gco stacks deploy-all -y${RESET}"
101219spacer
101319echo " ${DIM}Repository: https://github.com/aws-solutions-library-samples/global-capacity-orchestrator-on-aws${RESET}"
101419spacer
1015
1016# ── Cleanup Prompt ───────────────────────────────────────────────────────────
1017# Offer to delete all the demo jobs we just created. This keeps the cluster
1018# clean for the next demo run.
1019
102019echo " ${YELLOW}${BOLD}Clean up demo jobs?${RESET} ${DIM}(y/N)${RESET}"
102119if [ "${GCO_DEMO_NONINTERACTIVE:-}" = "1" ]; then
102218 cleanup="n"
1023else
10241 read -r cleanup
1025fi
102619case "$cleanup" in
1027 y|Y)
10281 narrate "Cleaning up demo jobs..."
1029 # Delete jobs by label (covers Volcano, FSx, EFS jobs with project=gco label)
10301 kubectl delete job -n gco-jobs -l project=gco --ignore-not-found=true 2>/dev/null || true
1031 # Delete specific jobs that may not have the project label
10321 kubectl delete job -n gco-jobs efs-output-example --ignore-not-found=true 2>/dev/null || true
10331 kubectl delete job -n gco-jobs valkey-cache-example --ignore-not-found=true 2>/dev/null || true
10341 kubectl delete job -n gco-jobs aurora-pgvector-example --ignore-not-found=true 2>/dev/null || true
10351 kubectl delete job -n gco-jobs kueue-sample-job --ignore-not-found=true 2>/dev/null || true
10361 kubectl delete job -n gco-jobs kueue-gpu-job --ignore-not-found=true 2>/dev/null || true
10371 kubectl delete job -n gco-jobs yunikorn-sample-job --ignore-not-found=true 2>/dev/null || true
10381 kubectl delete job -n gco-jobs yunikorn-gpu-job --ignore-not-found=true 2>/dev/null || true
10391 kubectl delete job -n gco-jobs yunikorn-gang-job --ignore-not-found=true 2>/dev/null || true
10401 kubectl delete job -n gco-jobs slurm-test --ignore-not-found=true 2>/dev/null || true
1041 # Volcano uses a custom resource type (vcjob), not a standard Job
10421 kubectl delete vcjob -n gco-jobs distributed-training --ignore-not-found=true 2>/dev/null || true
1043 # Clean up any downloaded files
10441 rm -rf /tmp/gco-demo-results 2>/dev/null || true
10451 success "Demo jobs cleaned up."
1046 ;;
1047 *)
104818 narrate "Skipping cleanup. Remove jobs manually with:"
104918 echo " ${CYAN}kubectl delete jobs --all -n gco-jobs${RESET}"
1050 ;;
1051esac
1052
105319spacer
105419echo " ${DIM}Thanks for watching.${RESET}"
105519spacer