Skip to content

tpu-inference/k8s: the TPU workload launcher - #467

Draft
theminghuang wants to merge 1 commit into
feature/tpu-multikueue-k8s-infrafrom
kube-launcher-final
Draft

tpu-inference/k8s: the TPU workload launcher#467
theminghuang wants to merge 1 commit into
feature/tpu-multikueue-k8s-infrafrom
kube-launcher-final

Conversation

@theminghuang

@theminghuang theminghuang commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

The TPU workload launcher, on top of #438. Every TPU step in tpu-inference#3377 runs it:

/opt/launcher/launch --profile v6e-8-2x4 -- pytest tests/

What it does

In a CPU-only agent pod, the launcher renders the workload manifest that lives in the repo under test - a Job, or a JobSet for a slice across hosts - fills in chip count, topology, node labels and queue from the profile registry generated here, forwards the step's BUILDKITE_* environment plus a named list, submits it to the profile's Kueue queue, waits for admission for as long as the total budget allows less one full run, polls the pod logs back over Connect Gateway (short requests, not a stream; keyed by owning Job so a preempt-and-resume reads as one log), mirrors the container's exit code, and files the log as a Buildkite artifact. Cancellation is a SIGTERM that uploads and deletes the workload; an ownerReference from the launcher pod covers the cases that never deliver one.

Why

agent-stack-k8s can only create a plain batch/v1 Job, and the agent must stay outside the Kueue workload: a preempted or disrupted run then pauses the step log and resumes rather than failing the build, and an admission wait holds a CPU pod rather than a TPU. Routing single-host work through the same path keeps one code path.

The split of responsibility is the point: the profile registry is generated from the same tfvars that build the node pools, so placement cannot drift from the queues; the manifest lives in the repo under test, so the shape of a job is a PR rather than an infrastructure change.

The manifest is untrusted input

Validated before submission: the Kueue queue label must be the profile's (quota cannot be side-stepped), serviceAccountName must be one the cluster publishes for workloads (tpu-workload; a workload may not run as the launcher and submit further work), the image must come from allowed_image_repos (the CI registry, set in tfvars), and there must be a container named workload. PodSecurity baseline on the namespace covers privileged, hostPath and host networking.

Files

  • kueue/launcher/launch.py - the launcher.
  • kueue/templates/launcher.yaml.tpl -> generated/manager/06-launcher.yaml: ServiceAccount, Role, the launcher and the profile registry as ConfigMaps, and the PodTemplate agent-stack-k8s runs it in. launcher_rbac_worker.yaml.tpl -> 07-launcher-rbac.yaml: a per-worker ClusterRole for pod logs.
  • scripts/generate_manifests.py - the launcher outputs; the profile registry (queue, chips, hosts, topology, accelerator label, max runtime, gcsfuse file-cache size per machine type); a check that a multi-host lane's quota is whole slices.
  • Validated with a four-host JobSet slice on v6e (kube-dev build 272: TPU_WORKER_HOSTNAMES injected, one 16-device mesh, vLLM TP=16 served) as well as the single-host suite; the multi-host profile itself is not declared in this stack yet.
  • iam.tf - Connect Gateway reader for the launcher identity on manager and workers; secret-scoped access to the Test Engine token.
  • buildkite.tf, variables.tf - the agent deadline derived from tpu_total_max_seconds; the analytics-token secret ids.
  • prod.auto.tfvars - allowed_image_repos.

@theminghuang

theminghuang commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

This change is part of the following stack:

Change managed by git-spice.

@theminghuang
theminghuang changed the base branch from kube-ci-infra to feature/tpu-multikueue-k8s-infra August 11, 2026 08:16
@theminghuang
theminghuang force-pushed the kube-launcher-final branch 4 times, most recently from 2e58524 to 414a29e Compare August 11, 2026 17:04
@theminghuang
theminghuang force-pushed the feature/tpu-multikueue-k8s-infra branch from d41056b to 9f72f1f Compare August 11, 2026 19:50
@theminghuang
theminghuang force-pushed the kube-launcher-final branch 10 times, most recently from 9b841ee to dff405d Compare August 11, 2026 21:37
@theminghuang
theminghuang force-pushed the feature/tpu-multikueue-k8s-infra branch from 4de0fe7 to 0fc31af Compare August 11, 2026 21:43
@theminghuang
theminghuang force-pushed the kube-launcher-final branch 2 times, most recently from 0ad44a9 to f2573ae Compare August 11, 2026 22:17
@theminghuang
theminghuang force-pushed the feature/tpu-multikueue-k8s-infra branch from 0fc31af to 81ecf54 Compare August 11, 2026 22:42
@theminghuang
theminghuang marked this pull request as draft August 11, 2026 22:53
@theminghuang
theminghuang force-pushed the feature/tpu-multikueue-k8s-infra branch from 81ecf54 to a6ea640 Compare August 11, 2026 22:57
@theminghuang
theminghuang force-pushed the kube-launcher-final branch 8 times, most recently from 0b94958 to 1c92d1b Compare August 14, 2026 04:57
@theminghuang
theminghuang force-pushed the feature/tpu-multikueue-k8s-infra branch from 6cd16c1 to 3255eb4 Compare August 14, 2026 05:01
@theminghuang
theminghuang force-pushed the kube-launcher-final branch 8 times, most recently from 689d5dc to 57235d3 Compare August 18, 2026 05:55
@theminghuang
theminghuang force-pushed the feature/tpu-multikueue-k8s-infra branch from 9cef2c8 to c4f616a Compare August 18, 2026 05:55
@theminghuang theminghuang changed the title feat(tpu-inference/k8s): TPU workload launcher tpu-inference/k8s: the TPU workload launcher Aug 18, 2026
@theminghuang
theminghuang force-pushed the feature/tpu-multikueue-k8s-infra branch from c4f616a to cf308ce Compare August 18, 2026 07:01
@theminghuang
theminghuang force-pushed the feature/tpu-multikueue-k8s-infra branch from 0e0bfb6 to 3d1f0cd Compare August 19, 2026 00:27
@theminghuang
theminghuang force-pushed the feature/tpu-multikueue-k8s-infra branch from 3d1f0cd to 128ca12 Compare August 19, 2026 00:35
@theminghuang
theminghuang force-pushed the kube-launcher-final branch 2 times, most recently from 8f10cc6 to d7afa9c Compare August 19, 2026 03:44
Every TPU step runs /opt/launcher/launch --profile <name> -- <command> in a
CPU-only agent pod. The launcher renders the workload manifest that lives in
the repo under test - a Job, or a JobSet for a slice across hosts - fills in
chip count, topology, node labels and queue from a profile registry
generated here, forwards the step's BUILDKITE_* environment plus a named
list, submits it to the profile's Kueue queue, waits for admission (as long
as the total budget allows less one full run), polls the pod logs back over
Connect Gateway, mirrors the container's exit code, and files the log as a
Buildkite artifact. Cancellation is a SIGTERM that uploads and deletes the
workload; an ownerReference from the launcher pod covers the cases that
never deliver one.

It exists because agent-stack-k8s can only create a plain Job, and because
the agent must stay outside the Kueue workload: a preempted or disrupted run
pauses the step log and resumes rather than failing the build, and an
admission wait holds a CPU pod, not a TPU.

The manifest is untrusted input and is validated: the Kueue queue label must
be the profile's, serviceAccountName must be one the cluster publishes for
workloads, the image must come from allowed_image_repos (the CI registry),
and there must be a container named workload. PodSecurity baseline on the
namespace covers the rest.

A manifest's ${NAME} placeholders are resolved in two passes, because the two
sides of the contract fail differently. The profile's nine names are required
and applied with substitute(), which raises if the manifest still holds an
unresolved name; the step's environment is optional and applied first with
safe_substitute(), so a manifest may reference ${BUILDKITE_COMMIT} or
${GANG_SIZE} without the launcher knowing about them. safe_substitute() alone
would leave a name nothing provided as the literal text "${NAME}", which
Kubernetes rejects nearly everywhere it can land - it is not a valid
quantity, integer or label value - but takes happily in a plain string like
an env value:, and the test then reads "${CHIPS}" as its chip count. One
consequence worth knowing: substitute() also rejects a lone `$` anywhere in
the file, comments included. Write `$$` for a literal one.

${ACCELERATOR_LABEL}, ${TOPOLOGY} and ${CHIPS} must additionally appear in
the manifest at all. The split above catches a name nothing provides, but not
a manifest that never mentions one, and placement is where that matters: a
manifest with a hardcoded or missing nodeSelector still requests TPU chips,
so Kueue admits it against the profile's quota and the pod takes any pool
with room - possibly a different topology than the profile promised. Nothing
rejects it and the run looks normal.

Cluster-side: 06-launcher.yaml (ServiceAccount, Role, the launcher script
and profile registry as ConfigMaps, the PodTemplate agent-stack-k8s uses)
and a per-worker ClusterRole for reading pod logs (07-launcher-rbac.yaml),
Connect Gateway reader and scoped Test Engine secret access for the launcher
identity, and the agent deadline derived from tpu_total_max_seconds. The
generator gains the launcher outputs, the profile registry (with the gcsfuse
file-cache size per machine type and hosts per slice), and a check that a
multi-host lane's quota is whole slices.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant