Skip to content

feat(aws/sagemaker): HyperPod — Slurm & EKS clusters, task governance, workload DX - #924

Merged
sam-goodwin merged 15 commits into
mainfrom
claude/hyperpod-support-audit-70571d
Jul 23, 2026
Merged

sam-goodwin merged 15 commits into
mainfrom
claude/hyperpod-support-audit-70571d

Conversation

@sam-goodwin

@sam-goodwin sam-goodwin commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Adds SageMaker HyperPod: the AWS.SageMaker.Cluster resource (Slurm or EKS orchestrated), task governance (ClusterSchedulerConfig + ComputeQuota), a first-class hyperpod prop on AWS.EKS.Job/Deployment, examples/aws-hyperpod covering every workload tier, and a dedicated docs page at /aws/compute/hyperpod (with EKS, choosing-a-runtime, and the AWS hub updated to point at it).

HyperPod is a persistent fleet; how work lands on it depends on the orchestrator, so the DX is tiered the same way ECS/EKS were in #867:

Slurm (default) EKS (orchestrator: { Eks })
Provision AWS.SageMaker.Cluster AWS.EKS.Cluster + AWS.SageMaker.Cluster
Low-level workloads ssm start-sessionsbatch AWS.EKS.Manifest (any K8s object)
High-level workloads — (no submission API) AWS.EKS.Job / Deployment with hyperpod: props
Governance Slurm accounting ClusterSchedulerConfig + ComputeQuota (Kueue)

Provision (both orchestrators)

// Slurm (default): sbatch over SSM, LifeCycleConfig required
const cluster = yield* AWS.SageMaker.Cluster("TrainingCluster", {
  instanceGroups: {
    controller: {
      InstanceType: "ml.t3.medium",
      InstanceCount: 1,
      ExecutionRole: role.roleArn,
      LifeCycleConfig: { SourceS3Uri: script.sourceS3Uri, OnCreate: "on_create.sh" },
    },
  },
});

// EKS: attach to a control plane; nodes join as ordinary EKS nodes
const hyperpod = yield* AWS.SageMaker.Cluster("HyperPod", {
  orchestrator: { Eks: { ClusterArn: eks.clusterArn } },
  vpcConfig: { SecurityGroupIds: [...], Subnets: network.privateSubnetIds },
  instanceGroups: { workers: { /* same shape — LifeCycleConfig required here too */ } },
});

Instance groups reconcile in place (removing one computes InstanceGroupsToDelete); VPC change replaces; createCluster retries the IAM propagation race on fresh execution roles like Lambda does.

Low level on Slurm — sbatch over SSM

Slurm has no remote submission API; jobs are submitted on the cluster. Every node is an SSM target:

aws sagemaker list-cluster-nodes --cluster-name <clusterName>
aws ssm start-session --target sagemaker-cluster:<cluster-id>_controller-<instance-id>
# on the node:
sbatch --nodes=4 train.sbatch

Low level on EKS — any Kubernetes object, pinned by node labels

const job = yield* AWS.EKS.Manifest("GovernedJob", {
  cluster: eks,
  manifest: {
    apiVersion: "batch/v1",
    kind: "Job",
    metadata: {
      namespace: Output.interpolate`hyperpod-ns-${quota.teamName}`,
      labels: { "kueue.x-k8s.io/queue-name": Output.interpolate`hyperpod-ns-${quota.teamName}-localqueue` },
    },
    spec: { template: { spec: {
      nodeSelector: {
        "sagemaker.amazonaws.com/node-health-status": "Schedulable",
        "sagemaker.amazonaws.com/instance-group-name": "workers",
      },
      containers: [{ name: "train", image: "ghcr.io/acme/train:v3" }],
    } } },
  },
});

High level on EKS — effectful Jobs/Deployments with hyperpod: props

The #867 surfaces run on HyperPod unchanged. The hyperpod prop references resources through the graph: instance groups are declared as a keyed object whose keys carry through to the cluster's attributes as types — hyperpod.instanceGroups.workers is typed per key (a typo'd name is a compile error) — and governance comes from the team's ComputeQuota resource, so the node selector, namespace, Kueue labels, and cluster/quota → workload ordering all derive from the data flow:

const hyperpod = yield* AWS.SageMaker.Cluster("HyperPod", {
  instanceGroups: { workers: { InstanceType: "ml.g5.xlarge", InstanceCount: 4, ... } },
  ...
});

const train = yield* AWS.EKS.Job(
  "Train",
  {
    cluster: eks,
    main: import.meta.url,
    hyperpod: {
      instanceGroup: hyperpod.instanceGroups.workers, // key-typed, Output-connected
      quota: researchQuota,      // → hyperpod-ns-research + Kueue queue label
      priorityClass: "training", // → training-priority
    },
  },
  Effect.gen(function* () {
    const putItem = yield* AWS.DynamoDB.PutItem(results); // IAM → pod identity
    return { run: Effect.gen(function* () { /* ... */ }) };
  }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),
);

Task governance

yield* AWS.EKS.Addon("TaskGovernance", { clusterName, addonName: "amazon-sagemaker-hyperpod-taskgovernance" });

const policy = yield* AWS.SageMaker.ClusterSchedulerConfig("Scheduler", {
  clusterArn: hyperpod.clusterArn,
  schedulerConfig: { PriorityClasses: [{ Name: "training", Weight: 75 }], FairShare: "Enabled" },
});

const quota = yield* AWS.SageMaker.ComputeQuota("ResearchQuota", {
  clusterArn: hyperpod.clusterArn,
  computeQuotaTarget: { TeamName: "research", FairShareWeight: 10 }, // creates hyperpod-ns-research
  computeQuotaConfig: { ComputeQuotaResources: [{ InstanceType: "ml.g5.xlarge", Count: 2 }] },
});

Verified live

  • Slurm: full cluster lifecycle (create → InService → destroy, ~5 min on 1× ml.t3.medium) behind AWS_TEST_SAGEMAKER_HYPERPOD=1.
  • EKS: the entire eks.run.ts stack deployed green against real AWS — network → EKS 1.34 → HyperPod dependencies chart → HyperPod attach (InService) → amazon-sagemaker-hyperpod-taskgovernance add-on → scheduler policy → team quota → the governed raw-manifest Job in hyperpod-ns-research → the effectful TrainJob via hyperpod: props.
  • Governance lifecycles (gated on AWS_TEST_SAGEMAKER_HYPERPOD_EKS_CLUSTER_ARN): ComputeQuota create → in-place update with version bump → destroy, all green live; ClusterSchedulerConfig create/reconcile verified via the stack, and the test asserts the typed one-policy-per-cluster conflict when the cluster is already governed (full lifecycle on a bare cluster). Ungated typed-error probes for all three resources run in every CI pass; full SageMaker suite 18/18.
  • Constraints discovered live, now encoded in the example + JSDoc: EKS auth mode must be API (CONFIG_MAP rejected), HyperPod trails the newest EKS version (1.28–1.35), LifeCycleConfig is required for EKS-orchestrated groups (docs claim otherwise), the HyperPod dependencies helm chart is mandatory pre-attach (its vendored legacy mpi-operator CRD fails k8s ≥1.34 strict SSA — disabled via values), and AWS allows one ClusterSchedulerConfig per cluster (typed as ClusterSchedulerConfigAlreadyExists via a distilled patch).
  • Drive-by provider fixes found by the live runs: EKS Job/CronJob names are capped at Kubernetes' 63-char limit (long app+stage combos were rejected via the batch.kubernetes.io/job-name label); EKS.Addon waits up to 20 min for node-bound addons and reports status in AddonNotReady; the K8s client retries transport failures and fresh-cluster auth propagation; EKS.Addon.read tolerates rows from crashed pre-resolve runs; EC2.InternetGateway delete retries DependencyViolation for ~5 min (EKS control planes release ENI addresses slowly).

🤖 Generated with Claude Code

…eQuota

Implements SageMaker HyperPod support: the Cluster resource (Slurm/EKS
orchestrated, in-place instance-group updates with computed
InstanceGroupsToDelete, VPC change => replace), plus the task-governance
ClusterSchedulerConfig and ComputeQuota resources (id-addressed with
list-by-name fallback, versioned updates via TargetVersion).

Full Slurm cluster lifecycle live-verified (create -> InService ->
destroy, ~5 min on 1x ml.t3.medium) behind AWS_TEST_SAGEMAKER_HYPERPOD=1;
governance lifecycles gate on AWS_TEST_SAGEMAKER_HYPERPOD_EKS_CLUSTER_ARN.
Ungated typed-error probes run in every CI pass. createCluster retries the
IAM propagation race on freshly created execution roles.

Adds examples/aws-hyperpod: bucket + role + deploy-time Action uploading
the lifecycle script + the cluster.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sam-goodwin
sam-goodwin marked this pull request as ready for review July 22, 2026 19:33
@alchemy-version-bot

alchemy-version-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

bun add alchemy@https://pkg.ing/alchemy/429e7ee

@alchemy.run/better-auth

bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/429e7ee

@alchemy.run/pr-package

bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/429e7ee

sam-goodwin and others added 5 commits July 22, 2026 13:19
…mple

- AWS.SageMaker.hyperpodScheduling: typed namespace/Kueue-label/nodeSelector
  fragments for running EKS workloads on HyperPod nodes (instance-group
  pinning, healthy-nodes-only, task-governance team + priority class).
- Cluster attrs expose orchestratorEksClusterArn; ComputeQuota attrs expose
  teamName (feeds hyperpod-ns-<team> references with a dependency edge).
- examples/aws-hyperpod/eks.run.ts: EKS-orchestrated stack covering all
  tiers — control plane, HyperPod attach, taskgovernance add-on, scheduler
  policy + team quota, a governed raw Manifest Job, and an effectful
  EKS.Job pinned via hyperpodScheduling.
- Cluster JSDoc now documents Slurm sbatch-over-SSM access and both EKS
  workload tiers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e runs

A deploy that fails before an Addon's Output-valued clusterName resolves
persists a row with the unresolved input stripped; the next plan's read
then called describeAddon with clusterName undefined and crashed the plan.
Return undefined (nothing observable) instead. Also require an explicit
control-plane role in the aws-hyperpod EKS example (EKS.Cluster requires
roleArn unless compute is "auto").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ive deploy

- LifeCycleConfig is required for EKS-orchestrated instance groups (the
  API rejects groups without it, contrary to the docs) — the EKS stack now
  carries the lifecycle bucket + upload Action like the Slurm stack.
- The orchestrating EKS cluster must use API (or API_AND_CONFIG_MAP)
  authentication mode; EKS's CONFIG_MAP default is rejected by
  CreateCluster. Pinned accessConfig explicitly.
- Documented both constraints in the Cluster JSDoc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… EKS version (supports 1.28-1.35)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… to ~5 min

A draining EKS/HyperPod control plane releases its public ENI addresses
over several minutes; the previous ~50s window reproducibly failed VPC
teardown after EKS cluster deletion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sam-goodwin sam-goodwin changed the title feat(aws/sagemaker): HyperPod Cluster, ClusterSchedulerConfig, ComputeQuota feat(aws/sagemaker): HyperPod — Slurm & EKS clusters, task governance, workload DX Jul 22, 2026
sam-goodwin and others added 9 commits July 22, 2026 14:54
…rpodScheduling helper

EKS workloads opt onto HyperPod nodes with a typed prop instead of a
spread helper. Passing the team's ComputeQuota resource derives the
hyperpod-ns-<team> namespace, Kueue queue/priority labels, and the
quota -> workload ordering from the data flow:

  yield* AWS.EKS.Job("Train", {
    cluster: eks,
    main: import.meta.url,
    hyperpod: { instanceGroup: "workers", quota, priorityClass: "training" },
  });

Effective-namespace changes (including hyperpod-derived) trigger
replacement keyed off a real prior row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ps, K8s client hardening

- hyperpod.instanceGroup accepts the same group object passed to the
  cluster's instanceGroups (one source of truth for the name) or a string.
- examples/aws-hyperpod: SageMaker validates the HyperPod dependencies
  helm chart before an EKS-orchestrated cluster attaches ("missing one or
  more required dependencies") — a FetchHyperPodChart Action clones
  aws/sagemaker-hyperpod-cli + vendors subcharts, and AWS.EKS.HelmChart
  applies it between the EKS cluster and the HyperPod attach.
- KubernetesApiError now carries method/path/status/body in its message,
  and applyObject retries transient failures (5xx/429 + the fresh-cluster
  401/403 access-entry propagation window) for ~1 min.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- waitForAddonActive budget 10 -> 20 min: node-bound addons (HyperPod task
  governance) stay DEGRADED until nodes join and pull images, and the wait
  can start early when upstream attrs resolve from a plan-time read.
  AddonNotReady now reports cluster/addon/status in its message.
- requestJson retries transport-level failures (ECONNREFUSED etc.) for
  ~40s — a fresh EKS endpoint can refuse connections briefly after ACTIVE.
  All requests on this path are idempotent (GET / SSA PATCH / DELETE).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…per-cluster conflict

- Content-addressed EKS Job names over 63 chars are rejected by the API
  (the name lands in the batch.kubernetes.io/job-name label) — truncate
  the base so the content-address suffix always fits. Found live: any
  long app+stage+id combination hit it.
- AWS allows ONE ClusterSchedulerConfig per HyperPod cluster; distilled
  now types the conflict (ClusterSchedulerConfigAlreadyExists) and the
  gated test asserts either the typed conflict (cluster already governed)
  or the full lifecycle (bare cluster). Verified live: ComputeQuota full
  lifecycle (create/update/destroy) green against a real EKS HyperPod
  cluster; ClusterSchedulerConfig create/reconcile green via the example
  stack and the conflict branch via the test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- /aws/compute/hyperpod: orchestrator choice (Slurm vs EKS), Slurm
  provisioning + sbatch-over-SSM, EKS orchestration constraints (API auth
  mode, version window, lifecycle scripts, mandatory dependencies chart),
  the hyperpod: workload prop, and task governance (one policy per
  cluster, team quotas).
- /aws/compute/eks: "Run on SageMaker HyperPod" section + link.
- Choosing-a-runtime EKS section and the AWS hub recipes table point at
  HyperPod; sidebar entry added.
- Merged main (docs from #925) so the edits land on the current text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- AWS.SageMaker.Cluster attributes expose `instanceGroups` keyed by group
  name; EKS workloads reference `hyperpod.instanceGroups.workers` so the
  workload is connected to the fleet through the resource graph instead
  of a shared object literal.
- Resource attribute accessors upgrade pure object-record attributes to
  ObjectExpr for typed nested Output access (scoped conditional —
  branded string unions and optionals stay plain Output to avoid the
  ToOutput string-method explosion).
- Docs: /aws/compute/hyperpod page, EKS "Run on SageMaker HyperPod"
  section now shows the cluster attach before the workload snippet, and
  all snippets use the Output-connected reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…te refs

instanceGroups (and restrictedInstanceGroups) are objects keyed by group
name instead of named arrays. A const-generic constructor overlay carries
the keys through to the cluster's attributes, so
`hyperpod.instanceGroups.workers` is typed per key — a typo'd group name
is a compile error — while the resource identity, provider registration,
and wire mapping (keyed object -> named array) stay unchanged.

  const hyperpod = yield* AWS.SageMaker.Cluster("HyperPod", {
    instanceGroups: { workers: { InstanceType: "ml.g5.xlarge", ... } },
  });
  hyperpod.instanceGroups.workers   // typed
  hyperpod.instanceGroups.workerz   // compile error

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sam-goodwin
sam-goodwin merged commit e13e36d into main Jul 23, 2026
4 checks passed
@sam-goodwin
sam-goodwin deleted the claude/hyperpod-support-audit-70571d branch July 23, 2026 06:44
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