Status:
operator runbookIntended use: create a disposable single-A100 AKS cluster, install TauGrid, and verify CUDA execution with device and training metrics. Not for: production clusters, shared clusters not provisioned for this purpose, or any cluster not intended for deletion afterward. An A100 node bills by the hour.
This example creates an AKS cluster with one NVIDIA A100 80GB node, installs TauGrid, and runs a PyTorch workload that requires GPU execution.
This is the GPU counterpart to aks-cpu-quickstart.
It uses the same three tools and adds one GPU node pool.
./examples/aks-gpu-quickstart/run.sh # ~25 min, creates billable resources
./examples/aks-gpu-quickstart/cleanup.sh # deletes everything
Cost warning. A
Standard_NC24ads_A100_v4node bills by the hour. Runcleanup.shafter collecting the required evidence.run.shprints the cleanup command when it finishes.
A workload can exit with status 0 after falling back to CPU. A successful
Kueue state verifies scheduling, not GPU computation. train.py uses three
checks that require GPU execution. Each check fails the run if its condition is
not met:
| Gate | Claim | CPU behavior |
|---|---|---|
| 1. Device identity | Compute capability 8.0 and ≥70 GiB VRAM, read inside the Ray training actor | torch.cuda.is_available() is False without a GPU; a non-A100 GPU reports a different capability |
| 2. Tensor-core throughput | ≥20 TFLOP/s sustained TF32 8192³ matmul | An A100 provides 150–300 TFLOP/s; a server CPU provides less than 5 TFLOP/s. The threshold allows a shared or throttled A100 and rejects CPU fallback. |
| 3. On-device convergence | Every parameter and every batch asserted .device.type == "cuda", and loss must decrease |
A GPU that is present but unused passes gates 1 and 2 and fails here |
On a CPU-only machine, probe_device() and train_loop() raise
GATE 1 FAILED.
Evidence is printed to stdout between === TAU-GPU-EVIDENCE-BEGIN === and
=== TAU-GPU-EVIDENCE-END === as JSON. Capture stdout as durable evidence. In
the default configuration, /data is an emptyDir, so files written to
TAU_DURABLE_CHECKPOINTS_DIR are deleted with the pod. Retrieve the output
with:
tau run logs tau-aks-gpu-quickstart -n taugrid-default --context tau-gpu-quickstart \
| sed -n '/TAU-GPU-EVIDENCE-BEGIN/,/TAU-GPU-EVIDENCE-END/p'az,tau,kubectlonPATH, and anaz loginsession.helm.tau cluster installinvokes it internally. Both Helm 3 and Helm 4 work with currenttau.- No chart checkout or registry login.
taupulls its pinned TauGrid chart from the public MCR OCI registry. - A100 quota in the target region. See below.
run.sh invokes exactly az, tau, and kubectl. It generates no Kubernetes
manifests and calls no other infrastructure tool.
A100 quota is specific to each VM family and region. run.sh checks quota in
step 0b. Detecting insufficient quota after cluster creation adds approximately
15 minutes and creates a billable resource group.
Check quota in the target region:
az vm list-usage --location <region> -o table | grep NCADS_A100_v4The default is swedencentral. NCADS_A100_v4 quota is region-specific, so
check where the subscription has capacity and override with
TAU_QUICKSTART_LOCATION.
Two quota constraints to check:
- The family must match the SKU.
Standard_NDASv4_A100quota does not admit anNC24adsnode, andStandard_ND96asr_v4needs 96 vCPU rather than 24. - Other clusters may consume the available quota. In a shared subscription, check regional usage before creating the node pool.
Default: Standard_NC24ads_A100_v4: 24 vCPU, 1× A100 80GB, the smallest
A100 shape on Azure. This workflow requires one node. An 8-GPU ND96 node
costs approximately eight times more per hour.
Tau keeps the Ray head pod CPU-only on the system pool and gives each
dedicated execution worker compute.gpus GPUs. With workers: 1 in tau.yaml
that means exactly one GPU pod, hence one GPU node. Setting workers: 2
requests a second A100 and therefore a second GPU node. It
fits a 50 vCPU quota (2 × 24) but doubles the hourly cost. The default uses one
worker.
TauGrid requires no GPU-specific configuration for this quickstart. The default chart includes GPU support:
charts/taugrid/values.yamlgives the baselinejobqueueClusterQueue anvidia.com/gpunominal quota.- Its
taugrid-defaultResourceFlavor selects onlykubernetes.io/os: linuxand declares no tolerations.
Create the A100 node pool without taints. run.sh uses this configuration.
Kueue cannot admit workloads to a tainted pool unless the chart defines the
required toleration.
1. AKS installs the driver but not the device plugin. gpuProfile.driver
defaults to Install, so the host driver is present and healthy. Nothing
advertises nvidia.com/gpu, so GPU pods remain Pending. run.sh
applies nvidia-device-plugin.yaml (step 2a), a digest-pinned MCR DaemonSet.
It needs privileged: true and a hostPath mount of /dev; with only
NVIDIA_VISIBLE_DEVICES=all it logs No devices found. Waiting indefinitely.
and never registers. This is a silent failure mode.
2. Standard_NC24ads_A100_v4 comes up with MIG enabled. az aks nodepool show reports gpuInstanceProfile: null, so this is not something AKS asked
for. It is the node/VM default. With MIG enabled and no MIG instances
configured, NVML detects the card but CUDA cannot create a context. Check these
results:
nvidia-smi -L→NVIDIA A100 80GB PCIecuInit()→ 100CUDA_ERROR_NO_DEVICEtorch.cuda.device_count()→1, buttorch.cuda.is_available()→False
nvidia-smi -mig 0 sets a pending state ("in use by another client"). Step 2c
restarts the node pool VMSS so the driver applies the change. The script skips
both operations when MIG is already Disabled.
Neither condition is diagnosed automatically. tau cluster validate nodes
discovers GPU nodes solely by reading allocatable nvidia.com/gpu, so both
failures produce the same output:
no GPU nodes found
Before MIG is disabled the node advertises no nvidia.com/gpu at all, so
validate nodes cannot identify the condition. Check the allocatable resource
directly. Step 2c automates this check:
# MIG mode on the node (Enabled here means CUDA cannot create a context)
kubectl get nodes -l agentpool=a100 \
-o jsonpath='{.items[*].status.allocatable.nvidia\.com/gpu}'Distinguishing "no GPU hardware" from "GPU hardware present but advertising nothing," including MIG enabled with no instances configured, and DRA
ResourceSlicenodes, would improvetau cluster validate nodes. This is not implemented today.
MIG is worth it only when you have many small single-GPU jobs. For jobs asking for one or more whole GPUs — the common case, and what this example does — leave MIG off. On A100s, changing MIG mode requires a node restart. MIG with instances configured is supported through
compute.gpu_resource_mode: mig; only the empty state is a fault.
The only GPU-specific settings live in the researcher's tau.yaml:
compute:
gpus: 1
runtime:
image: mcr.microsoft.com/aks/ai-runtime/ray:py3.12-ray2.56.0-cuda13.0
pip: [torch>=2.4.0]The image must be the -cuda tag. The CPU quickstart's plain
py3.12-ray2.54.0 ships no CUDA runtime and gate 1 will fail on it. The default
PyPI torch wheel bundles its own CUDA runtime libraries, so no extra index URL
is needed. It only needs a recent enough host driver, which the AKS GPU node
image provides.
tau.yaml requests one GPU. compute.gpus and compute.workers control the
shape, and tau picks the launcher from them:
workers |
gpus |
Launcher | What drives the ranks |
|---|---|---|---|
| 1 | 1 | python3 |
single process |
| 1 | N | torchrun --standalone --nproc_per_node=N |
one process per GPU, one pod |
| >1 | N | python3 |
the script drives Ray Train across pods |
train.py handles the first two directly: it detects RANK/LOCAL_RANK/
WORLD_SIZE (set only by torchrun), pins cuda:$LOCAL_RANK, and wraps the
model in DistributedDataParallel instead of calling Ray Train. Only rank 0
prints the evidence block, so the output stays readable.
Verified on 8x A100-SXM4-80GB in one pod: ranks 0-7 each bound to their own
GPU, driver_visible: 8, 124.1 TFLOP/s, loss 4.1620 -> 0.9972, backend nccl.
On a mixed cluster Kueue picks a ResourceFlavor automatically, which may not be
the intended hardware. There is no --gpu-class or --queue flag on tau run;
select the flavor in the config instead:
policy:
queue: jobqueue
node_selector:
kueue.azure.com/gpu-series: ndm-a100-v4 # from `kubectl get resourceflavor`Match the key/value to the flavor's spec.nodeLabels.
scheduling.node_selector is not a field, and the two config
schemas in this repo fail differently on it. Verified both ways:
| Config schema | Used by | Unknown top-level key |
|---|---|---|
managed workflow (schema_version: 1) |
this example's tau.yaml |
silently ignored: a scheduling: typo fails open and the job lands anywhere |
direct run config (engine:) |
../aks-cpu-quickstart/stellar-demo/tau.yaml |
hard error: field scheduling not found in type runconfig.Config |
The managed workflow schema silently ignores a misspelled key. Verify that the selector is present in the rendered manifest:
tau run --config tau.yaml --namespace taugrid-default --dry-run=client \
| grep -A2 nodeSelectorNodes joined via AKS Flex (kubernetes.azure.com/cluster=flex-*) had no route
to PyPI in testing: pip install torch fails with
[Errno 101] Network is unreachable. runtime.pip is therefore unusable
there. This differs from the permission issue in "Known issues" below because
the installation never reaches the package index. The --user fallback and a
writable package directory do not resolve this condition. Include dependencies
in a custom runtime.image. Managed node pools in the same cluster had egress.
Verify egress for each node pool before using runtime.pip.
| Step | Command | Note |
|---|---|---|
| 0 | tau run --dry-run=client |
renders the RayJob offline |
| 0b | az vm list-usage |
fails fast on insufficient quota |
| 1 | az group create, az aks create |
CPU-only system pool |
| 2 | az aks nodepool add |
the A100 pool, untainted |
| 2a | kubectl apply -f nvidia-device-plugin.yaml |
AKS does not install this |
| 2b | kubectl get nodes poll |
waits for nvidia.com/gpu to be allocatable |
| 2c | nvidia-smi -mig 0 + az vmss restart |
MIG is on by default; CUDA fails until it is off |
| 3 | tau cluster install, tau cluster validate installation |
|
| 4 | tau workspace create taugrid-default --apply |
NAME is optional; pass it explicitly to keep overrides aligned |
| 5 | tau run --config tau.yaml |
submit the workload |
| 6 | tau run status / tau run logs |
Step 2b handles a timing issue. A GPU node reports Ready before the device
plugin registers the device. Submitting during this interval leaves the pod
Pending with Insufficient nvidia.com/gpu. The script waits for the resource
to become allocatable.
Steps 4–6 do not require --workspace or --namespace. TauGrid v0 creates one
workspace per cluster with a default name.
./examples/aks-gpu-quickstart/cleanup.shThe script cancels the workload, deletes the workspace, uninstalls TauGrid, and deletes the resource group. Resource group deletion stops billing. All cleanup steps are idempotent and can be rerun after a partial failure.
Set TAU_QUICKSTART_KEEP_CLUSTER=1 to keep the cluster for another run. The
script warns that the A100 keeps billing.
-
runtime.pipruns under a non-root runtime. Tau retries a failed system install withpip install --userand adds the matching user-sitebindirectory toPATH. This supportstorchand its console scripts on the pinned Ray images. Package-index egress is still a separate prerequisite; use an image with dependencies preinstalled when the node cannot reach the approved index. -
#1288 On Helm 4,
tau cluster installanduninstallused to fail on server-side apply conflicts: AKS'sadmissionsenforceraddon co-owns.webhooks[*].namespaceSelectoron Kueue's webhook configurations. Both verbs now send--force-conflictswhen the installed Helm accepts it, which resolves the ownership standoff without changing what AKS put there.run.shstill passes--wait=false --atomic=falseand gates ontau cluster validate installation. -
#1275 A completed Ray job holds its node capacity for the TTL window. On a single-GPU cluster a second submission will sit unadmitted until the first job's RayCluster is reclaimed.