Baladithya Balamurugan Claude Opus 4.8 (1M context) commited on
Commit
7453f13
·
1 Parent(s): 41289bf

Wave 20: SageMaker GRPO smoke artifacts (F3) — runnable g5.2xlarge GSM8K

Browse files

The F3 runnable-now RL smoke, with two live-verified corrections to the
design doc's recipe:
- DLC tag is cu126 + requires the -v1.25 build suffix (NOT the cu124 bare
tag in the AWS docs page; resolved against the live ECR registry).
- sagemaker SDK pinned <3: v3 is an API rewrite that dropped
sagemaker.estimator.Estimator / sagemaker.pytorch.

Artifacts:
- examples/gsm8k_grpo/run_sagemaker.py: GPU+vLLM-colocate variant of run.py
(same GSM8K RLVR reward, plain GRPO alpha=beta=0), CLI/JSON hyperparams,
writes to SM_MODEL_DIR.
- examples/gsm8k_grpo/run_sagemaker_launch.py: sagemaker-v2 Estimator driver.
Default --image dlc uses the stock PyTorch DLC + source_dir (no local
15GB cross-arch build); --image <ecr> uses a prebuilt baked image.
Stages a minimal source_dir (framework + script + requirements).
- examples/gsm8k_grpo/requirements.txt: trl + vllm==0.8.5 (torch-2.6 line) +
RL stack, installed at job start atop the DLC's torch 2.6/cu126.
- docker/Dockerfile.sagemaker + scripts/build_and_push_ecr.sh: the
repeatable baked-image path (corrected DLC tag, cross-arch note).
- docs/AWS_SAGEMAKER_QUICKSTART.md: live facts + run instructions + gotchas.
- pyproject [aws] extra: add sagemaker>=2.200,<3.

Live preflight verified: g5.2xlarge training quota=1, exec role trusts
sagemaker, output bucket writable, 0 jobs running. Smoke job submitted
(composer-grpo-smoke-2026-06-09-18-35-31-353); result recorded separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

docker/Dockerfile.sagemaker ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Baked SageMaker training image for the Composer-replication RL stack (F3 §3.2).
2
+ #
3
+ # The repeatable path: bake trl+vllm+the framework into an image so jobs don't
4
+ # pip-install at startup (saves ~5-10 min/job and removes a flaky failure
5
+ # surface). The one-shot smoke can instead use the stock DLC + source_dir
6
+ # (run_sagemaker_launch.py --image dlc), which needs no local build.
7
+ #
8
+ # Base: AWS PyTorch DLC, tag RESOLVED LIVE against the us-west-2 registry
9
+ # 2026-06-09 — it is cu126 (NOT the cu124 some docs list) and the -v1.25 build
10
+ # suffix is required (no bare floating 2.6.0-gpu tag exists).
11
+ FROM 763104351884.dkr.ecr.us-west-2.amazonaws.com/pytorch-training:2.6.0-gpu-py312-cu126-ubuntu22.04-sagemaker-v1.25
12
+
13
+ # RL stack baked in. torch 2.6 + CUDA 12.6 already in the DLC — do NOT reinstall
14
+ # torch. vllm 0.8.5 is the torch-2.6 line (pin to avoid a wheel/CUDA mismatch).
15
+ RUN pip install --no-cache-dir \
16
+ "trl>=1.5,<2" "peft>=0.13" "accelerate>=1.0" "datasets>=3.0" \
17
+ "vllm==0.8.5" "fsspec>=2024.6" "s3fs>=2024.6" "hf_transfer>=0.1.6"
18
+
19
+ # The framework itself (train + serverless extras → trainer, loss, executors,
20
+ # replica_entrypoint, s3fs all present).
21
+ COPY . /opt/composer_replication
22
+ RUN pip install --no-cache-dir -e "/opt/composer_replication[train,serverless]"
23
+
24
+ ENV HF_HOME=/opt/ml/input/hf_cache \
25
+ HF_HUB_ENABLE_HF_TRANSFER=1
docs/AWS_SAGEMAKER_QUICKSTART.md ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AWS SageMaker Quickstart — the runnable-now GRPO smoke
2
+
3
+ The minimum path to running the Composer-replication RL inner loop on a real
4
+ GPU, end-to-end, for **under $1**. Implements F3 (`research/design-F3-rl-sagemaker.md`).
5
+
6
+ ## Live account facts (verified 2026-06-09, acct 386931836011, us-west-2)
7
+
8
+ | Fact | Value |
9
+ |---|---|
10
+ | `ml.g5.2xlarge` training-job quota | **1** (live, code `L-2D6DEB3C`) → no quota ticket needed |
11
+ | Execution role | `arn:aws:iam::386931836011:role/service-role/AmazonSageMaker-ExecutionRole-20250725T133247` |
12
+ | Bucket (rendezvous + output) | `amazon-sagemaker-386931836011-us-west-2-7597bf4d9a3d` |
13
+ | PyTorch DLC base image | `763104351884.dkr.ecr.us-west-2.amazonaws.com/pytorch-training:2.6.0-gpu-py312-cu126-ubuntu22.04-sagemaker-v1.25` |
14
+
15
+ > **Two corrections vs the AWS DLC docs page** (found by querying the live ECR
16
+ > registry, not the docs): the tag is **cu126**, not cu124, and the **`-v1.25`
17
+ > build suffix is required** — there is no bare floating `2.6.0-gpu...` tag.
18
+ > Always resolve the tag against the registry:
19
+ > ```bash
20
+ > aws ecr describe-images --registry-id 763104351884 \
21
+ > --repository-name pytorch-training --region us-west-2 \
22
+ > --query "reverse(sort_by(imageDetails,&imagePushedAt))[].imageTags" --output text \
23
+ > | tr '\t' '\n' | grep -E '^2.6.0-gpu-py312-cu126-.*-sagemaker-v[0-9.]+$' | head -1
24
+ > ```
25
+
26
+ > **SDK pin:** the smoke launcher uses the **sagemaker SDK v2** Estimator API.
27
+ > SDK **v3 is an API rewrite** that dropped `sagemaker.estimator.Estimator` and
28
+ > `sagemaker.pytorch` — install `pip install 'sagemaker>=2.200,<3'`.
29
+
30
+ ## Run it (no local Docker build)
31
+
32
+ ```bash
33
+ pip install 'sagemaker>=2.200,<3'
34
+ export AWS_REGION=us-west-2
35
+ python examples/gsm8k_grpo/run_sagemaker_launch.py --max-steps 20
36
+ ```
37
+
38
+ This uses the **stock PyTorch DLC directly** as the training image and ships the
39
+ framework + entry script via `source_dir`; `examples/gsm8k_grpo/requirements.txt`
40
+ (trl + vllm==0.8.5 + the RL stack) installs at job start. No 15 GB local image
41
+ build, no ECR push. The script trains `Qwen/Qwen2.5-0.5B-Instruct` with GRPO +
42
+ the GSM8K `#### NUMBER` RLVR reward, vLLM **colocated** in-process
43
+ (`vllm_gpu_memory_utilization=0.3` on the 24 GB A10G).
44
+
45
+ Flags: `--no-wait` (submit + poll later), `--spot` (managed spot, quota=1 too),
46
+ `--no-vllm` (fall back to `model.generate` rollout if a vLLM/CUDA wheel mismatch
47
+ appears), `--image <ecr-uri>` (use a prebuilt baked image instead of the DLC).
48
+
49
+ **Cost:** `ml.g5.2xlarge` ≈ $1.52/hr on-demand; a 20-step 0.5B smoke is
50
+ ~15–25 min ⇒ **well under $1**. Spot ≈ $0.45–0.60/hr ⇒ pennies.
51
+
52
+ ## The repeatable path (baked image)
53
+
54
+ For runs where the ~5–10 min per-job pip-install is unwanted (and for the
55
+ DiLoCo N-replica `SageMakerExecutor`, which passes `ContainerEntrypoint` and
56
+ needs the framework baked in), build the image once:
57
+
58
+ ```bash
59
+ scripts/build_and_push_ecr.sh # creates ECR repo, builds, pushes composer-rl:smoke
60
+ python examples/gsm8k_grpo/run_sagemaker_launch.py \
61
+ --image 386931836011.dkr.ecr.us-west-2.amazonaws.com/composer-rl:smoke
62
+ ```
63
+
64
+ On an Apple-Silicon host the build cross-compiles (`--platform linux/amd64`) the
65
+ ~15 GB GPU DLC under emulation — slow; prefer a linux/amd64 host or CodeBuild.
66
+
67
+ ## Gotchas (load-bearing)
68
+
69
+ - **`EnableNetworkIsolation` stays False** (the default) so the container can
70
+ reach `huggingface.co` (model + GSM8K download) and S3.
71
+ - **`vllm_gpu_memory_utilization=0.3`** is the load-bearing knob on a 24 GB
72
+ A10G: too high → OOM when the policy + grads also need the GPU; too low → tiny
73
+ KV cache. Use `--no-vllm` if a vLLM wheel/CUDA mismatch surfaces.
74
+ - **Warm pools are off** — `g5 training warm pool usage` quota is 0 in this
75
+ account, so each job pays ~3–6 min cold-start. Request a warm-pool quota bump
76
+ for iterative dev, or move the long inner loop to HyperPod (persistent).
77
+ - **"Waiting for capacity"** in `SecondaryStatus` is transient g5 capacity
78
+ contention in the region, not an error — the job proceeds when capacity frees.
79
+
80
+ ## Next: DiLoCo N-replica (the `SageMakerExecutor` path)
81
+
82
+ `examples/diloco_sagemaker/run.py` (driver, F3 §4.3) drives N independent
83
+ single-instance Training Jobs sharing one `s3://.../rendezvous/` prefix via
84
+ `ObjectStoreAllReduce` — no cross-job NCCL. N=1 runs today; N=2–4 needs a
85
+ `ml.g5.2xlarge for training job usage` quota increase. The DiLoCo math, loss,
86
+ trainer, and `ObjectStoreAllReduce` are unchanged from the smoke — the S3
87
+ rendezvous is the entire portability contract (validated `file://` and live
88
+ `s3://`; see `test_serverless_local.py::test_s3_rendezvous_allreduce_across_replicas`).
89
+ ```
examples/gsm8k_grpo/requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Installed by SageMaker at job start, layered on the PyTorch DLC 2.6.0
2
+ # (torch 2.6 + CUDA 12.6 already present — do NOT reinstall torch).
3
+ # F3 §3.2: baking these into a Dockerfile.sagemaker is the repeatable path;
4
+ # this requirements.txt is the no-local-build path for the one-shot smoke.
5
+ trl>=1.5,<2
6
+ peft>=0.13
7
+ accelerate>=1.0
8
+ datasets>=3.0
9
+ # vLLM must match the DLC's torch 2.6 / cu126. 0.8.x is the torch-2.6 line;
10
+ # pinning avoids a silent wheel/CUDA mismatch at colocate time (F3 §7).
11
+ vllm==0.8.5
12
+ fsspec>=2024.6
13
+ s3fs>=2024.6
14
+ hf_transfer>=0.1.6
examples/gsm8k_grpo/run_sagemaker.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GPU + vLLM-colocated variant of run.py, for a SageMaker Training Job.
2
+
3
+ F3 §3.3: the runnable-NOW GRPO smoke. Same GSM8K RLVR reward and same
4
+ `ComposerReplicationTrainer(alpha_sdpo=0, beta_replay=0)` (plain GRPO, channels
5
+ 2/3 off) as the CPU example, lifted to one real GPU with vLLM colocated in the
6
+ training process. Proves: container builds, trainer runs on GPU, vLLM rollout
7
+ works, reward fires, checkpoint lands in S3.
8
+
9
+ This script runs INSIDE the SageMaker container. SageMaker conventions used:
10
+ * Hyperparameters arrive as CLI args ``--key value`` (the Estimator's
11
+ ``hyperparameters=`` dict). We also read /opt/ml/input/config/
12
+ hyperparameters.json as a fallback.
13
+ * The final model must be written to ``/opt/ml/model`` (SM_MODEL_DIR);
14
+ SageMaker tars it to ``OutputDataConfig.S3OutputPath`` on exit.
15
+ * stdout/stderr stream to CloudWatch ``/aws/sagemaker/TrainingJobs/<job>``.
16
+
17
+ Run via examples/gsm8k_grpo/run_sagemaker_launch.py (the Estimator driver).
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import logging
24
+ import os
25
+ import re
26
+ import sys
27
+
28
+ import torch
29
+ from datasets import load_dataset
30
+ from transformers import AutoModelForCausalLM, AutoTokenizer
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Reward — identical RLVR `#### NUMBER` regex to the CPU example (run.py)
34
+ # ---------------------------------------------------------------------------
35
+
36
+ _ANSWER_RE = re.compile(r"####\s*(-?\d+(?:\.\d+)?)")
37
+
38
+
39
+ def _extract_answer(text: str) -> str | None:
40
+ matches = _ANSWER_RE.findall(text or "")
41
+ return matches[-1].strip() if matches else None
42
+
43
+
44
+ def gsm8k_reward(completions, **kwargs):
45
+ gold = kwargs.get("gold_answer")
46
+ if gold is None:
47
+ return [0.0] * len(completions)
48
+ rewards: list[float] = []
49
+ for completion, gold_ans in zip(completions, gold, strict=False):
50
+ if isinstance(completion, list):
51
+ text = "\n".join(m.get("content", "") for m in completion)
52
+ else:
53
+ text = str(completion)
54
+ pred = _extract_answer(text)
55
+ rewards.append(1.0 if (pred is not None and pred == str(gold_ans).strip()) else 0.0)
56
+ return rewards
57
+
58
+
59
+ SYSTEM_PROMPT = (
60
+ "You are a math tutor. Solve the problem step by step. "
61
+ "End your answer with `#### N` where N is the final numeric answer."
62
+ )
63
+
64
+
65
+ def build_dataset(n_rows: int):
66
+ raw = load_dataset("openai/gsm8k", "main", split=f"train[:{n_rows}]")
67
+
68
+ def _format(row):
69
+ gold = _extract_answer(row["answer"]) or ""
70
+ return {
71
+ "prompt": [
72
+ {"role": "system", "content": SYSTEM_PROMPT},
73
+ {"role": "user", "content": row["question"]},
74
+ ],
75
+ "gold_answer": gold,
76
+ }
77
+
78
+ return raw.map(_format, remove_columns=raw.column_names)
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Hyperparameters — SageMaker passes them as CLI args; JSON fallback.
83
+ # ---------------------------------------------------------------------------
84
+
85
+
86
+ def _parse_hyperparameters() -> argparse.Namespace:
87
+ p = argparse.ArgumentParser()
88
+ p.add_argument("--model", default="Qwen/Qwen2.5-0.5B-Instruct")
89
+ p.add_argument("--n_train_rows", type=int, default=100)
90
+ p.add_argument("--max_steps", type=int, default=20)
91
+ p.add_argument("--num_generations", type=int, default=8)
92
+ p.add_argument("--per_device_train_batch_size", type=int, default=8)
93
+ p.add_argument("--max_completion_length", type=int, default=256)
94
+ p.add_argument("--learning_rate", type=float, default=1e-5)
95
+ p.add_argument("--beta", type=float, default=0.04)
96
+ p.add_argument("--vllm_gpu_memory_utilization", type=float, default=0.3)
97
+ p.add_argument("--use_vllm", type=lambda s: str(s).lower() != "false", default=True)
98
+ # SageMaker model output dir (env SM_MODEL_DIR, default /opt/ml/model).
99
+ p.add_argument("--model_dir", default=os.environ.get("SM_MODEL_DIR", "/opt/ml/model"))
100
+ args, _unknown = p.parse_known_args()
101
+ return args
102
+
103
+
104
+ def main() -> int:
105
+ logging.basicConfig(
106
+ level=logging.INFO,
107
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
108
+ handlers=[logging.StreamHandler(sys.stdout)],
109
+ )
110
+ log = logging.getLogger("gsm8k_grpo_sagemaker")
111
+ args = _parse_hyperparameters()
112
+
113
+ log.info("=" * 64)
114
+ log.info("GRPO + GSM8K + %s (SageMaker GPU, vLLM=%s)", args.model, args.use_vllm)
115
+ log.info("=" * 64)
116
+ log.info("hyperparameters: %s", json.dumps(vars(args), indent=2))
117
+
118
+ cuda = torch.cuda.is_available()
119
+ log.info("CUDA available: %s | device: %s", cuda,
120
+ torch.cuda.get_device_name(0) if cuda else "cpu")
121
+
122
+ log.info("[1/4] Loading model + tokenizer ...")
123
+ tokenizer = AutoTokenizer.from_pretrained(args.model)
124
+ if tokenizer.pad_token_id is None:
125
+ tokenizer.pad_token = tokenizer.eos_token
126
+ model = AutoModelForCausalLM.from_pretrained(
127
+ args.model, torch_dtype=torch.bfloat16 if cuda else torch.float32
128
+ )
129
+
130
+ log.info("[2/4] Loading %d GSM8K rows ...", args.n_train_rows)
131
+ dataset = build_dataset(args.n_train_rows)
132
+
133
+ log.info("[3/4] Building GRPOConfig + ComposerReplicationTrainer ...")
134
+ from trl import GRPOConfig
135
+
136
+ from composer_replication import ComposerReplicationTrainer
137
+
138
+ config = GRPOConfig(
139
+ output_dir=args.model_dir,
140
+ per_device_train_batch_size=args.per_device_train_batch_size,
141
+ num_generations=args.num_generations,
142
+ max_completion_length=args.max_completion_length,
143
+ learning_rate=args.learning_rate,
144
+ max_steps=args.max_steps,
145
+ logging_steps=1,
146
+ save_strategy="no",
147
+ report_to=[],
148
+ bf16=cuda,
149
+ beta=args.beta,
150
+ # vLLM colocated in-process on the same GPU (F3 §3.3 / §5).
151
+ use_vllm=bool(args.use_vllm and cuda),
152
+ vllm_mode="colocate",
153
+ vllm_gpu_memory_utilization=args.vllm_gpu_memory_utilization,
154
+ vllm_tensor_parallel_size=1,
155
+ seed=42,
156
+ )
157
+
158
+ trainer = ComposerReplicationTrainer(
159
+ model=model,
160
+ processing_class=tokenizer,
161
+ reward_funcs=[gsm8k_reward],
162
+ train_dataset=dataset,
163
+ args=config,
164
+ alpha_sdpo=0.0, # channels 2/3 off — plain GRPO smoke
165
+ beta_replay=0.0,
166
+ )
167
+
168
+ log.info("[4/4] Training for %d steps ...", args.max_steps)
169
+ result = trainer.train()
170
+ log.info("Training complete: %s", result.metrics)
171
+
172
+ # Persist to SM_MODEL_DIR → SageMaker uploads to OutputDataConfig.
173
+ trainer.save_model(args.model_dir)
174
+ log.info("Model saved to %s (SageMaker will upload to S3).", args.model_dir)
175
+ return 0
176
+
177
+
178
+ if __name__ == "__main__":
179
+ sys.exit(main())
examples/gsm8k_grpo/run_sagemaker_launch.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Launch the GSM8K GRPO smoke as a SageMaker Training Job (F3 §3.4).
2
+
3
+ Two ways to run, selected by --image:
4
+ * --image dlc (default): use the AWS PyTorch DLC directly as the training
5
+ image and ship the framework + script via source_dir; deps install at job
6
+ start from requirements.txt. No local Docker build — the lowest-friction
7
+ path for a one-shot smoke (F3's "source_dir on top of the baked image",
8
+ here on top of the *stock* DLC).
9
+ * --image <ecr-uri>: use a prebuilt baked image (docker/Dockerfile.sagemaker
10
+ pushed to ECR via scripts/build_and_push_ecr.sh) — the repeatable path that
11
+ avoids the ~5-10 min startup pip-install (F3 §3.2).
12
+
13
+ Live facts (verified 2026-06-09, acct 386931836011, us-west-2):
14
+ * ml.g5.2xlarge training-job quota = 1 → runs with no quota ticket.
15
+ * DLC tag resolved live: 2.6.0-gpu-py312-cu126-ubuntu22.04-sagemaker-v1.25
16
+ (NOTE: cu126, not the cu124 in some docs; bare floating tag does not exist,
17
+ the -v1.25 build suffix is required).
18
+ * role: AmazonSageMaker-ExecutionRole-20250725T133247
19
+ * bucket: amazon-sagemaker-386931836011-us-west-2-7597bf4d9a3d
20
+
21
+ Usage (from the laptop / Studio, sagemaker SDK v2):
22
+ pip install 'sagemaker>=2.200,<3'
23
+ python examples/gsm8k_grpo/run_sagemaker_launch.py --max-steps 20
24
+ python examples/gsm8k_grpo/run_sagemaker_launch.py --no-wait # fire and poll later
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import os
30
+ import shutil
31
+ import sys
32
+ import tempfile
33
+
34
+ REGION = "us-west-2"
35
+ ACCOUNT = "386931836011"
36
+ ROLE = f"arn:aws:iam::{ACCOUNT}:role/service-role/AmazonSageMaker-ExecutionRole-20250725T133247"
37
+ BUCKET = f"amazon-sagemaker-{ACCOUNT}-{REGION}-7597bf4d9a3d"
38
+ # DLC tag resolved live against the 763104351884 us-west-2 registry.
39
+ DLC_IMAGE = (
40
+ "763104351884.dkr.ecr.us-west-2.amazonaws.com/"
41
+ "pytorch-training:2.6.0-gpu-py312-cu126-ubuntu22.04-sagemaker-v1.25"
42
+ )
43
+
44
+ _HERE = os.path.dirname(os.path.abspath(__file__))
45
+ _REPO_ROOT = os.path.abspath(os.path.join(_HERE, "..", ".."))
46
+
47
+
48
+ def _stage_source() -> str:
49
+ """Build a minimal source_dir: the entry script + requirements.txt + the
50
+ composer_replication package (importable as a local package from
51
+ /opt/ml/code). Keeps the S3 upload tiny — no .venv/research/docs/.git.
52
+ Returns the staging dir path (caller cleans up)."""
53
+ staging = tempfile.mkdtemp(prefix="composer-sm-src-")
54
+ shutil.copy2(os.path.join(_HERE, "run_sagemaker.py"), staging)
55
+ shutil.copy2(os.path.join(_HERE, "requirements.txt"), staging)
56
+ shutil.copytree(
57
+ os.path.join(_REPO_ROOT, "composer_replication"),
58
+ os.path.join(staging, "composer_replication"),
59
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "tests", "*.egg-info"),
60
+ )
61
+ return staging
62
+
63
+
64
+ def main() -> int:
65
+ ap = argparse.ArgumentParser()
66
+ ap.add_argument("--image", default="dlc",
67
+ help="'dlc' (stock PyTorch DLC + source_dir) or a prebuilt ECR image URI")
68
+ ap.add_argument("--instance-type", default="ml.g5.2xlarge")
69
+ ap.add_argument("--max-steps", type=int, default=20)
70
+ ap.add_argument("--n-train-rows", type=int, default=100)
71
+ ap.add_argument("--model", default="Qwen/Qwen2.5-0.5B-Instruct")
72
+ ap.add_argument("--no-vllm", action="store_true",
73
+ help="disable colocated vLLM (use model.generate rollout) — safer fallback")
74
+ ap.add_argument("--spot", action="store_true", help="use managed spot (quota=1 too)")
75
+ ap.add_argument("--no-wait", action="store_true", help="submit and return; poll later")
76
+ ap.add_argument("--max-run", type=int, default=3600)
77
+ args = ap.parse_args()
78
+
79
+ import sagemaker
80
+ from sagemaker.estimator import Estimator
81
+
82
+ sess = sagemaker.Session(default_bucket=BUCKET)
83
+ image = DLC_IMAGE if args.image == "dlc" else args.image
84
+ print(f"[launch] region={REGION} image={image}")
85
+ print(f"[launch] role={ROLE}")
86
+ print(f"[launch] instance={args.instance_type} max_steps={args.max_steps} "
87
+ f"vllm={not args.no_vllm} spot={args.spot}")
88
+
89
+ staging = _stage_source()
90
+ print(f"[launch] staged source_dir at {staging}")
91
+
92
+ hyperparameters = {
93
+ "model": args.model,
94
+ "n_train_rows": args.n_train_rows,
95
+ "max_steps": args.max_steps,
96
+ "use_vllm": "false" if args.no_vllm else "true",
97
+ }
98
+
99
+ spot_kwargs = {}
100
+ if args.spot:
101
+ spot_kwargs = {"use_spot_instances": True, "max_wait": args.max_run + 3600}
102
+
103
+ est = Estimator(
104
+ image_uri=image,
105
+ role=ROLE,
106
+ instance_type=args.instance_type,
107
+ instance_count=1,
108
+ volume_size=100,
109
+ max_run=args.max_run,
110
+ sagemaker_session=sess,
111
+ output_path=f"s3://{BUCKET}/composer-rl/smoke/output",
112
+ base_job_name="composer-grpo-smoke",
113
+ entry_point="run_sagemaker.py",
114
+ source_dir=staging,
115
+ hyperparameters=hyperparameters,
116
+ environment={
117
+ "HF_HUB_ENABLE_HF_TRANSFER": "1",
118
+ # DLC sagemaker-training-toolkit installs requirements.txt from source_dir.
119
+ },
120
+ # EnableNetworkIsolation MUST be False (default) so the container can reach
121
+ # huggingface.co (model + GSM8K) and S3 (F3 §3.5).
122
+ keep_alive_period_in_seconds=0, # warm-pool quota=0 in this acct → leave off
123
+ **spot_kwargs,
124
+ )
125
+
126
+ try:
127
+ est.fit(wait=not args.no_wait, logs=("All" if not args.no_wait else None))
128
+ finally:
129
+ shutil.rmtree(staging, ignore_errors=True)
130
+
131
+ print(f"[launch] job name: {est.latest_training_job.name}")
132
+ if not args.no_wait:
133
+ print(f"[launch] model artifact: {est.model_data}")
134
+ return 0
135
+
136
+
137
+ if __name__ == "__main__":
138
+ sys.exit(main())
pyproject.toml CHANGED
@@ -75,8 +75,13 @@ eks = [
75
  "kubernetes>=29",
76
  ]
77
  # Amazon SageMaker training-job executor (SageMakerExecutor, per ADR-005).
 
 
 
 
78
  aws = [
79
  "boto3>=1.34",
 
80
  ]
81
  # Replaysim dataset normalization (per ADR-004)
82
  #
 
75
  "kubernetes>=29",
76
  ]
77
  # Amazon SageMaker training-job executor (SageMakerExecutor, per ADR-005).
78
+ # boto3: the executor uses raw create_training_job. sagemaker (<3): the v2
79
+ # Estimator API the GSM8K smoke launcher (examples/gsm8k_grpo/
80
+ # run_sagemaker_launch.py) uses — pinned <3 because SDK v3 is an API rewrite
81
+ # that dropped sagemaker.estimator.Estimator (F3 §1, verified live 2026-06-09).
82
  aws = [
83
  "boto3>=1.34",
84
+ "sagemaker>=2.200,<3",
85
  ]
86
  # Replaysim dataset normalization (per ADR-004)
87
  #
scripts/build_and_push_ecr.sh ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Build + push the baked SageMaker RL image to this account's private ECR (F3 §3.2).
3
+ # One-time (Admin). The one-shot smoke does NOT need this — use
4
+ # python examples/gsm8k_grpo/run_sagemaker_launch.py --image dlc
5
+ # which runs on the stock DLC + source_dir with no local build. Use this script
6
+ # for the repeatable path (no per-job pip-install) and the DiLoCo N-replica
7
+ # executor (which passes ContainerEntrypoint and wants the framework baked in).
8
+ #
9
+ # NOTE: the DLC base is GPU/linux-amd64 (~15 GB). On an Apple-Silicon host you
10
+ # must cross-build: pass --platform linux/amd64 (set below). This is slow under
11
+ # emulation; prefer building on a linux/amd64 host or CodeBuild for real use.
12
+ set -euo pipefail
13
+
14
+ REGION="${REGION:-us-west-2}"
15
+ ACCOUNT="${ACCOUNT:-386931836011}"
16
+ REPO="${REPO:-composer-rl}"
17
+ TAG="${TAG:-smoke}"
18
+ DLC_ACCOUNT="763104351884"
19
+
20
+ REGISTRY="${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com"
21
+ IMAGE="${REGISTRY}/${REPO}:${TAG}"
22
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
23
+
24
+ echo "[ecr] region=${REGION} image=${IMAGE}"
25
+
26
+ # 1. Ensure the ECR repo exists (idempotent).
27
+ aws ecr describe-repositories --repository-names "${REPO}" --region "${REGION}" >/dev/null 2>&1 \
28
+ || aws ecr create-repository --repository-name "${REPO}" --region "${REGION}" >/dev/null
29
+
30
+ # 2. Log in to BOTH the DLC registry (to pull the base) and our own (to push).
31
+ aws ecr get-login-password --region "${REGION}" \
32
+ | docker login --username AWS --password-stdin "${DLC_ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com"
33
+ aws ecr get-login-password --region "${REGION}" \
34
+ | docker login --username AWS --password-stdin "${REGISTRY}"
35
+
36
+ # 3. Build (cross-arch on Apple Silicon) + push.
37
+ docker build --platform linux/amd64 \
38
+ -f "${REPO_ROOT}/docker/Dockerfile.sagemaker" \
39
+ -t "${IMAGE}" "${REPO_ROOT}"
40
+ docker push "${IMAGE}"
41
+
42
+ echo "[ecr] pushed ${IMAGE}"
43
+ echo "[ecr] launch with: python examples/gsm8k_grpo/run_sagemaker_launch.py --image ${IMAGE}"