1/**
2 * A minimal SageMaker HyperPod training cluster:
3 *
4 * - a lifecycle-script bucket + instance execution role (`src/infra.ts`),
5 * - a deploy-time Action that uploads `on_create.sh` to the bucket
6 * (`src/lifecycle.ts`) — bucket → script → cluster ordering is inferred
7 * from the data flow,
8 * - a Slurm-orchestrated `AWS.SageMaker.Cluster` with one instance group.
9 *
10 * The demo instance group is a single ml.t3.medium to keep the example
11 * cheap; swap in ml.g5/ml.p5 groups (and more of them) for real training.
12 * Provisioning takes ~5 minutes at this size, 10-25 minutes for large GPU
13 * groups.
14 */
15import * as Alchemy from "alchemy";
16import * as AWS from "alchemy/AWS";
17import * as Effect from "effect/Effect";
18import { HyperPodInfra } from "./src/infra.ts";
19import { UploadLifecycleScript } from "./src/lifecycle.ts";
20
21export default Alchemy.Stack(
22 "AwsHyperPodExample",
23 {
24 providers: AWS.providers(),
25 state: Alchemy.localState(),
26 },
27 Effect.gen(function* () {
28 const { bucket, role } = yield* HyperPodInfra;
29
30 // Upload the on_create script; the Action's output feeds the cluster's
31 // LifeCycleConfig, so the script is in place before the cluster exists.
32 const script = yield* UploadLifecycleScript({
33 bucketName: bucket.bucketName,
34 });
35
36 const cluster = yield* AWS.SageMaker.Cluster("TrainingCluster", {
37 instanceGroups: {
38 controller: {
39 InstanceType: "ml.t3.medium",
40 InstanceCount: 1,
41 ExecutionRole: role.roleArn,
42 LifeCycleConfig: {
43 SourceS3Uri: script.sourceS3Uri,
44 OnCreate: script.onCreate,
45 },
46 },
47 },
48 // Automatic recovery replaces faulty nodes; "None" is fine for a demo
49 // controller group and avoids replacement churn on tiny instances.
50 nodeRecovery: "None",
51 tags: { app: "aws-hyperpod-example" },
52 });
53
54 return {
55 clusterName: cluster.clusterName,
56 clusterArn: cluster.clusterArn,
57 clusterStatus: cluster.clusterStatus,
58 lifecycleBucket: bucket.bucketName,
59 };
60 }),
61);