Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

apiVersion: v2
name: a4x_max_jobset_workload
description: a4x_max_jobset_workload
type: application
version: 0.1.0
appVersion: "1.16.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<!-- mdformat global-off -->
# Pretrain qwen3-30b-a3b workloads on a4x-max GKE Node pools with Nvidia Megatron-Bridge Framework

This recipe outlines the steps for running a qwen3-30b-a3b pretraining
workload on [a4x-max GKE Node pools](https://cloud.google.com/kubernetes-engine) by using the
[Megatron-Bridge pretraining workload](https://github.com/NVIDIA-NeMo/Megatron-Bridge).

## Orchestration and deployment tools

For this recipe, the following setup is used:

- Orchestration - [Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine)
- Pretraining job configuration and deployment - A Helm chart is used to
configure and deploy the [Kubernetes Jobset](https://kubernetes.io/blog/2025/03/23/introducing-jobset) resource which manages the execution of the
[Megatron-Bridge pretraining workload](https://github.com/NVIDIA-NeMo/Megatron-Bridge).

## Test environment

This recipe has been optimized for and tested with the following configuration:

- GKE cluster
Please follow Cluster Toolkit [instructions](https://github.com/GoogleCloudPlatform/cluster-toolkit/)
to create your a4x-max GKE cluster.

## Training dataset

This recipe uses a mock pretraining dataset provided by the Megatron-Bridge framework.

## Docker container image

This recipe uses the following docker images:

- `nvcr.io/nvidia/nemo:26.06.01`
**Installed Plugins:**
- `nccl-gib-plugins` version: 1.1.2-1

## Run the recipe

From your client workstation, complete the following steps:

### Configure environment settings

Set the environment variables to match your environment:

```bash
export PROJECT_ID=<PROJECT_ID>
export CLUSTER_REGION=<CLUSTER_REGION>
export CLUSTER_NAME=<CLUSTER_NAME>
export GCS_BUCKET=<GCS_BUCKET> # Note: path should not be prefixed with gs://
export KUEUE_NAME=<KUEUE_NAME>
export HF_TOKEN=<YOUR_HF_TOKEN>
```

Replace the following values:

- `<PROJECT_ID>`: your Google Cloud project ID.
- `<CLUSTER_REGION>`: the region where your cluster is located.
- `<CLUSTER_NAME>`: the name of your GKE cluster.
- `<GCS_BUCKET>`: the name of your Cloud Storage bucket. Don't include the `gs://` prefix.
- `<KUEUE_NAME>`: the name of the Kueue local queue. The default queue created by the cluster toolkit is `a4x-max`. Make sure to verify the name of the local queue in your cluster.
- `<YOUR_HF_TOKEN>`: Your HuggingFace token.

Set the default project:

```bash
gcloud config set project $PROJECT_ID
```

### Get the recipe

Clone the `gpu-recipes` repository and set a reference to the recipe folder.

```
git clone https://github.com/ai-hypercomputer/gpu-recipes.git
cd gpu-recipes
export REPO_ROOT=`git rev-parse --show-toplevel`
export RECIPE_ROOT=$REPO_ROOT/training/a4x-max/qwen3-30b-a3b/megatron-bridge-gke/nemo2606/8gpus-fp8mx-seq4096-gbs512/recipe
cd $RECIPE_ROOT
```

### Get cluster credentials

```
gcloud container clusters get-credentials $CLUSTER_NAME --region $CLUSTER_REGION
```

### Configure and submit a pretraining job

#### Using 2 node (8 gpus) fp8mx precision
To execute the job with the default settings, run the following command from
your client:

```bash
cd $RECIPE_ROOT
export WORKLOAD_NAME=$USER-a4x-max-qwen3-30b-a3b-8gpus
helm install $WORKLOAD_NAME . -f values.yaml \
--set-file workload_launcher=launcher.sh \
--set workload.image=nvcr.io/nvidia/nemo:26.06.01 \
--set volumes.gcsMounts[0].bucketName=${GCS_BUCKET} \
--set volumes.gcsMounts[0].mountPath=/job-logs \
--set workload.envs[0].value=/job-logs/$WORKLOAD_NAME \
--set queue=${KUEUE_NAME}
```

**Examples**

- To set the number of training steps to 100, run the following command from
your client:

```bash
cd $RECIPE_ROOT
export WORKLOAD_NAME=$USER-a4x-max-qwen3-30b-a3b-8gpus
helm install $WORKLOAD_NAME . -f values.yaml \
--set-file workload_launcher=launcher.sh \
--set workload.image=nvcr.io/nvidia/nemo:26.06.01 \
--set volumes.gcsMounts[0].bucketName=${GCS_BUCKET} \
--set volumes.gcsMounts[0].mountPath=/job-logs \
--set workload.envs[0].value=/job-logs/$WORKLOAD_NAME \
--set queue=${KUEUE_NAME} \
--set workload.arguments[0]="trainer.max_steps=100"
```

### Monitor the job

To check the status of pods in your job, run the following command:

```
kubectl get pods | grep $USER-a4x-max-qwen3-30b-a3b-8gpus
```

Replace the following:

- JOB_NAME_PREFIX - your job name prefix. For example $USER-a4x-max-qwen3-30b-a3b-8gpus.

To get the logs for one of the pods, run the following command:

```
kubectl logs POD_NAME
```

Information about the training job's progress, including crucial details such as
loss, step count, and step time, is generated by the rank 0 process.
This process runs on the pod whose name begins with
`JOB_NAME_PREFIX-workload-0-0`.
For example: `$USER-a4x-max-qwen3-30b-a3b-8gpus-workload-0-0-s9zrv`.

### Uninstall the Helm release

You can delete the job and other resources created by the Helm chart. To
uninstall Helm, run the following command from your client:

```bash
helm uninstall $USER-a4x-max-qwen3-30b-a3b-8gpus
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
usage()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The script does not enable errexit (-e) or pipefail (-o pipefail). Since shell options are not inherited by child shell processes, any failure inside this script (such as git clone failing or torchrun crashing) will not cause the script to exit immediately. Specifically, because torchrun is piped to python3 for logging, without pipefail enabled, the exit status of the pipeline will be the exit status of the python3 command (which is 0 on successful EOF), completely masking any training failures from torchrun. This will cause Kubernetes to report the job as successful even if the training crashed. Enable set -eo pipefail at the very beginning of launcher.sh to ensure failures are correctly propagated.

Suggested change
usage()
#!/bin/bash
set -eo pipefail
usage()

{
cat << EOF
usage: bash ./launcher.sh [config-override [config-override ...]]
config-override (Optional) A NeMo configuration override. E.g. trainer.max_steps=10000.
EOF
}

parse_args() {
while [[ "$1" != "" ]]; do
case $(grep -o "=" <<< "$1" | wc -l) in
1 )
config_overrides+=("$1")
;;
* )
echo "Invalid config override: $1"
usage
exit 1
esac
shift
done
config_overrides="${config_overrides[*]}"
}

config_overrides=()
parse_args "$@"

if [[ -z "${config_overrides[*]}" ]]; then
echo "No NeMo config overrides specified"
else
echo "NeMo config overrides:"
echo " ${config_overrides}"
fi

export LD_LIBRARY_PATH="/usr/local/cuda/compat/lib:$NCCL_PLUGIN_PATH:$LD_LIBRARY_PATH"
ldconfig "$LD_LIBRARY_PATH"
echo "Added $LD_LIBRARY_PATH to ldconfig:"
ldconfig -p | grep libcuda | sed 's/^/ /'
echo ""

if [[ -n "${EXPLICIT_LOG_DIR}" ]]; then
explicit_log_dir="${EXPLICIT_LOG_DIR}"
else
explicit_log_dir="workload_logs"
fi

# Ensure explicit_log_dir is an absolute path before any cd commands
if [[ "$explicit_log_dir" != /* ]]; then
explicit_log_dir="${PWD}/${explicit_log_dir}"
fi
echo "Logging to ${explicit_log_dir}"

if [[ -n "${TOKENIZER_PATH}" ]]; then
echo "Getting tokenizer files"
cp "${TOKENIZER_PATH}"/* .
echo ""
fi

echo "Launching Torch distributed on the node rank $JOB_COMPLETION_INDEX out of $NNODES nodes"

# Create the nsys directory.
mkdir -p "${explicit_log_dir}/nsys"

# Collect diagnostics
linux_kv="$(uname --kernel-release)"
cuda_driver_v=""
driver_v=""
vbios_v=""
if command -v nvidia-smi &> /dev/null; then
cuda_driver_v=$(nvidia-smi -q -x | grep -Po '(?<=<cuda_version>).*(?=</cuda_version>)' || true)
driver_v=$(nvidia-smi -q -x | grep -Po '(?<=<driver_version>).*(?=</driver_version>)' || true)
vbios_v=$(nvidia-smi -q -x | grep -Po '(?<=<vbios_version>).*(?=</vbios_version>)' | head -n1 || true)
fi
nccl_v=$(python3 -c "import torch; v=torch.cuda.nccl.version() if hasattr(torch.cuda, 'nccl') else 'unknown'; print('.'.join(map(str, v)) if isinstance(v, tuple) else v)" || echo "unknown")
cuda_container_v=$(python3 -c "import torch; print(torch.version.cuda)" || echo "unknown")

kv="{\"linux_kernel_version\": \"${linux_kv}\""
kv="${kv}, \"cuda_driver_version\": \"${cuda_driver_v}\""
kv="${kv}, \"cuda_container_version\": \"${cuda_container_v}\""
kv="${kv}, \"gpu_driver_version\": \"${driver_v}\""
kv="${kv}, \"vbios_version\": \"${vbios_v}\""
kv="${kv}, \"nccl_version\": \"${nccl_v}\"}"

echo "VERSION_DIAGNOSTICS: ${kv}"


export HF_TOKEN=<YOUR_HF_TOKEN>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The hardcoded export HF_TOKEN=<YOUR_HF_TOKEN> in launcher.sh will overwrite any valid HF_TOKEN passed to the container from Kubernetes environment variables with the literal string <YOUR_HF_TOKEN>. Since launcher.sh is uploaded as-is via helm install --set-file, this will break Hugging Face authentication inside the pod. Allow HF_TOKEN to be inherited from the environment if already set, and avoid overwriting it.

Suggested change
export HF_TOKEN=<YOUR_HF_TOKEN>
export HF_TOKEN="${HF_TOKEN:-}"

export PYTHONUNBUFFERED=1
export CUDA_DEVICE_MAX_CONNECTIONS=32
export CUDNNFE_CLUSTER_OVERLAP_MARGIN=8
export HF_HUB_OFFLINE=0
export NCCL_GRAPH_REGISTER=0
export NCCL_NVLS_ENABLE=0
export NUM_OF_HYBRID_EP_RANKS_PER_NVLINK_DOMAIN=8
export NUM_OF_TOKENS_PER_CHUNK_COMBINE_API=128
export NVLINK_DOMAIN_SIZE=72
export NVTE_BWD_LAYERNORM_SM_MARGIN=20
export NVTE_CUTEDSL_FUSED_GROUPED_MLP=1
export NVTE_FWD_LAYERNORM_SM_MARGIN=20
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True,graph_capture_record_stream_reuse:True
export TOKENIZERS_PARALLELISM=False
export TORCH_NCCL_AVOID_RECORD_STREAMS=0
export TORCH_NCCL_HIGH_PRIORITY=1
export TRANSFORMERS_OFFLINE=1
export USE_MNNVL=1
export NCCL_RAS_ENABLE=0

cd /opt
rm -rf Megatron-Bridge
git clone https://github.com/NVIDIA-NeMo/Megatron-Bridge.git
cd Megatron-Bridge
git checkout 5cb3444c43f7499cf3872b2d46870cf8bc2e00ce
git submodule update --init --recursive
Comment on lines +108 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Cloning the Megatron-Bridge repository and updating submodules at runtime inside the launcher script is prone to network failures, GitHub rate-limiting (especially when multiple nodes clone concurrently), and increased startup latency. Pre-clone this repository and build it into the container image, or package it as part of the workspace/volume.

sed -i -e '/pretrain(config=recipe/i \ recipe.dist.distributed_timeout_minutes = 10' scripts/performance/run_script.py
ls



worker_command=$(cat <<- EOM
if [ "\$RANK" -eq "0" ]; then
echo "Worker 0 is stalling for a few seconds.." ;
sleep 3 ;
echo "The detected environment within worker rank 0 is:" ;
env | sed 's/^/ /' ;
else
echo "Worker \$RANK is running" ;
fi ;

cd /opt/Megatron-Bridge ;

export MEGATRON_CONFIG_LOCK_DIR="/tmp/\$LOCAL_RANK"
mkdir -p "\$MEGATRON_CONFIG_LOCK_DIR"

numactl \
--cpunodebind=\$((LOCAL_RANK/2)) \
--membind=\$((LOCAL_RANK/2)) \
nice -10 \
python scripts/performance/run_script.py \
--model_family_name qwen \
--model_recipe_name qwen3_30b_a3b \
--config_variant v1 \
--gpu gb300 \
--num_gpus 8 \
--gpus_per_node 4 \
--compute_dtype fp8_mx \
--seq_length 4096 \
--global_batch_size 512 \
--micro_batch_size 8 \
--tensor_model_parallel_size 1 \
--pipeline_model_parallel_size 1 \
--context_parallel_size 1 \
--expert_model_parallel_size 8 \
--expert_tensor_parallel_size 1 \
--cuda_graph_impl local \
--cuda_graph_scope full_iteration \
--moe_a2a_overlap true \
--moe_flex_dispatcher_backend hybridep \
--max_steps 50 \
logger.log_throughput=True \
train.manual_gc_interval=100
EOM
)

echo "$worker_command" > worker_command.sh
chmod 777 worker_command.sh

torchrun \
--nproc-per-node="4" \
--nnodes="2" \
--node_rank="${JOB_COMPLETION_INDEX}" \
--rdzv_id="${JOB_IDENTIFIER}" \
--master_addr="${MASTER_ADDR}" \
--master_port="${MASTER_PORT}" \
--no-python bash worker_command.sh 2>&1 | python3 -u -c "import sys, time; [sys.stdout.write('[{}] {}'.format(time.strftime('%Y-%m-%d %H:%M:%S'), line)) for line in iter(sys.stdin.readline, '')]"



if [[ "$JOB_COMPLETION_INDEX" == "0" ]]; then
mkdir -p "${ARTIFACT_DIR}"
cp -r "${explicit_log_dir}"/* "${ARTIFACT_DIR}/"
env > "${ARTIFACT_DIR}/environ.txt"
ls "${ARTIFACT_DIR}"
fi
echo "Training completed"
echo "Pod on $(hostname --fqdn) is exiting"
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# yamllint disable
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

{{- if .Values.workload.configFile }}
apiVersion: v1
kind: ConfigMap
metadata:
name: "{{ .Release.Name }}-config"
data:
workload-configuration: |-
{{- if .Values.workload_config }}
{{ .Values.workload_config | nindent 4 }}
{{- else }}
{{ "config: null" | nindent 4 }}
{{- end }}
{{- end }}
Loading