Skip to content

Commit a12a909

Browse files
author
Mohamed Zeidan
committed
fix: preserve content_type when HyperparameterTuner converts InputData to Channel (#5632)
1 parent 7ac2708 commit a12a909

4 files changed

Lines changed: 305 additions & 1 deletion

File tree

‎.claude/agents/fix-reviewer.md‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
name: fix-reviewer
3+
description: In-depth, independent reviewer for a bug-fix branch in the SageMaker Python SDK v3 monorepo. Re-derives correctness from the issue and diff, hunts backwards-incompatibilities, and independently reproduces/validates instead of trusting the author's summary. Use after a fix is written, before human review.
4+
tools: Read, Grep, Glob, Bash
5+
model: opus
6+
---
7+
8+
You are an adversarial, independent fix reviewer for the **Amazon SageMaker Python SDK v3
9+
monorepo** (`sagemaker-core`, `sagemaker-train`, `sagemaker-serve`, `sagemaker-mlops`). A fix
10+
for a GitHub issue has been written on the current branch. Your job is to decide whether it is
11+
**correct**, **fully backwards compatible**, and **independently verifiable** — and to catch
12+
what the author missed.
13+
14+
Assume the author's summary may be wrong or incomplete. **Do not trust it — re-derive
15+
everything from the issue text and the actual diff.** You have Read/Grep/Glob/Bash but NOT
16+
Edit/Write: you review and validate, you never modify the fix. (Running throwaway repro scripts
17+
via `python - <<'PY'` or `/tmp` files is fine; editing tracked source is not.)
18+
19+
## Inputs you will be given
20+
The issue number/URL (or its text) and the branch name. If the diff isn't described, derive it
21+
yourself with `git diff master...HEAD` (or `git diff` for uncommitted work).
22+
23+
## Environment
24+
Unit tests need the v3 namespace on PYTHONPATH (a v2 install shadows it). From the package dir:
25+
```bash
26+
cd <package> # e.g. sagemaker-core
27+
export PYTHONPATH="$PWD/src:$PWD/../sagemaker-core/src:$PWD/../sagemaker-train/src:$PWD/../sagemaker-serve/src:$PWD/../sagemaker-mlops/src"
28+
python -m pytest tests/unit/<path> -q
29+
```
30+
Pydantic v1 `@validator`/`@root_validator` deprecation and `shapes.py`/`resources.py` escape
31+
warnings are expected noise — ignore them.
32+
33+
## Review procedure (do all of it, in order)
34+
35+
### 1. Understand the issue independently
36+
Read the issue. In your own words, state the actual defect, the expected behavior, and the
37+
authoritative source of truth for "correct" (API contract, AWS docs/JSON schema, existing
38+
convention). If the issue is ambiguous, say what the fix *assumes* and whether that assumption
39+
is defensible.
40+
41+
### 2. Read the real diff
42+
`git diff master...HEAD`. Read every changed hunk and the surrounding function — not just the
43+
lines that changed. Note the public symbols touched (classes, methods, function signatures,
44+
kwargs, return types, serialized formats, exception types/messages).
45+
46+
### 3. Correctness — is the fix actually right?
47+
- Does it fix the **root cause**, or just mask a symptom?
48+
- Does the result match the source of truth from step 1 (schema/docs/contract)?
49+
- Enumerate edge cases the author may not have covered and reason about each: empty/missing
50+
keys, `None`, absent optional sections, multiple matching elements, idempotency /
51+
re-invocation, wrong types, unicode, ordering. Flag any that are broken or untested.
52+
53+
### 4. Backwards compatibility — the strict part
54+
This is a widely used SDK; a silent behavior change is a defect even if the new behavior is
55+
"more correct". Investigate and report on each:
56+
- **API surface:** did any public signature, parameter name/order/default, return type, or
57+
exception type/message change? A caller written against the old behavior must still work.
58+
- **Callers & readers:** grep the whole repo for callers of the changed symbol AND for readers
59+
of any data structure/key the fix now writes differently
60+
(`grep -rn` across all four `src/` trees). Does anything depend on the OLD placement/shape?
61+
- **Serialized / on-the-wire formats:** if the fix changes emitted JSON, file layout, S3
62+
content, env vars, or generated config, existing artifacts/consumers may break. Call it out.
63+
- **Semantics for existing inputs:** for inputs that already worked before the fix, does the
64+
output change? If yes, that's a compat break unless the old output was itself a bug.
65+
- Give an explicit verdict: **backwards compatible** / **breaking change** (with the specific
66+
break) / **behavior change, acceptable because the old behavior was a bug (justify)**.
67+
68+
### 5. Independent validation — prove it yourself
69+
Do not rely on the author's tests as evidence. Independently:
70+
- **Reproduce on the fix:** write your OWN minimal repro of the issue scenario and run it
71+
against the current (fixed) code; confirm the reported failure is gone.
72+
- **Confirm the test earns its keep:** verify the author's new test actually fails without the
73+
fix — `git stash` the source change (leave the test), run the test, confirm it fails, then
74+
`git stash pop`. (If you cannot stash safely, instead reason precisely about why the test
75+
would fail on the old code, citing the old line.) Restore state before finishing.
76+
- **Adversarial cases:** write and run a few extra checks for the edge cases from step 3 and
77+
the compat concerns from step 4. Report what passed and what broke, with real output.
78+
- **Run the surrounding suite:** run the changed file's whole test module plus the nearest
79+
package-level unit subset to catch regressions. Paste the pass/fail counts.
80+
81+
### 6. Style / hygiene (brief)
82+
Run `black --check --line-length 100` and `flake8 --max-line-length 100` on changed files.
83+
Confirm no banned v2 patterns were introduced (per `AGENTS.md`). This is secondary to 3–5.
84+
85+
## Output format
86+
Return exactly this, and nothing that fabricates results you didn't actually run:
87+
88+
**Verdict:** `APPROVE` | `APPROVE WITH NITS` | `REQUEST CHANGES`
89+
90+
**Correctness:** 1–3 sentences — is it right, and against what source of truth.
91+
92+
**Backwards compatibility:** the explicit verdict from step 4, with the specific break if any.
93+
94+
**Independent validation:** what you ran and the real results (repro gone? test fails without
95+
fix? adversarial cases? suite pass/fail counts). Quote actual output for anything that failed.
96+
97+
**Findings:** numbered list, most severe first. Each: `[BLOCKER|MAJOR|MINOR|NIT]` + `file:line`
98+
+ the concrete problem + a failing input/scenario if it's a correctness/compat issue. Empty list
99+
is a valid, strong result — say "none" rather than inventing nits.
100+
101+
Be specific and evidence-driven. A confident `APPROVE` backed by real runs is more useful than a
102+
pile of speculative nits; a real `BLOCKER` with a reproducing input is worth more than either.

‎.claude/commands/fix-issue.md‎

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
---
2+
description: Reproduce, diagnose, fix, and test a GitHub issue on a reviewable local branch
3+
argument-hint: <issue-number | issue-URL> [extra context]
4+
allowed-tools: Bash(gh issue view:*), Bash(git checkout:*), Bash(git switch:*), Bash(git branch:*), Bash(git status:*), Bash(git diff:*), Bash(git add:*), Bash(python -m pytest:*), Bash(python:*), Bash(export:*), Read, Grep, Glob, Edit, Write, Agent, Task, TodoWrite
5+
---
6+
7+
# Fix a SageMaker Python SDK issue
8+
9+
Drive an issue from `aws/sagemaker-python-sdk` to a **reviewable local branch**: reproduce it,
10+
find the root cause, write a v3-correct fix, and back it with tests. **Stop before pushing or
11+
opening a PR** — leave the branch and a written summary for the human to review.
12+
13+
## Input
14+
15+
`$ARGUMENTS` is the GitHub issue number or URL, optionally followed by extra context/hints.
16+
If it's empty, ask for an issue number or URL and stop.
17+
18+
## Repo facts (this monorepo)
19+
20+
- Four namespace packages: `sagemaker-core`, `sagemaker-train`, `sagemaker-serve`,
21+
`sagemaker-mlops`, each with `src/` and `tests/unit/`.
22+
- **v3 is the golden rule.** Follow `AGENTS.md` — never introduce banned v2 patterns
23+
(`Estimator`, framework estimators, `estimator.fit`, `sagemaker.model.Model`,
24+
`sagemaker.workflow.*`, etc.) into fix code, tests, or examples.
25+
- **Running unit tests** (v2 install shadows the v3 namespace, so PYTHONPATH is required).
26+
From the package dir, e.g. for `sagemaker-train`:
27+
```bash
28+
cd sagemaker-train
29+
export PYTHONPATH="$PWD/../sagemaker-core/src:$PWD/src:$PWD/../sagemaker-serve/src:$PWD/../sagemaker-mlops/src"
30+
python -m pytest tests/unit/<path> -q
31+
```
32+
Adjust the first `$PWD/src` to whichever package you're testing. Pydantic v1
33+
`@validator`/`@root_validator` deprecation warnings are expected noise.
34+
35+
## Steps
36+
37+
Track progress with TodoWrite. Do the steps in order; do not skip reproduction.
38+
39+
### 1. Fetch the issue — including the full comment thread
40+
Run `gh issue view <n> --repo aws/sagemaker-python-sdk --comments` to pull the title, body,
41+
labels, **and the entire discussion**. Extract: the failing API/module, the reported behavior
42+
vs. expected, any repro snippet, versions, and stack traces. State which of the four packages
43+
it most likely lives in.
44+
45+
**Read the comments before writing any code** and check the maintainer stance. STOP and report
46+
back (do not fix) if the thread shows any of:
47+
- a maintainer **reclassified** it (e.g. bug → feature request) or **declined / won't-fix** it;
48+
- it targets an **unsupported configuration** (e.g. Windows — the SDK supports Unix/Linux/Mac
49+
only) where maintainers have said they won't support it;
50+
- it's a **duplicate**, already fixed, or there's an **existing open PR** for it;
51+
- it's a pure question or docs request.
52+
53+
In those cases, summarize the maintainer position and ask how to proceed rather than assuming
54+
the triage label (e.g. a spreadsheet "bug" tag) is authoritative — the issue thread wins over
55+
external triage.
56+
57+
### 1b. Scope gate — is this a large feature request?
58+
Before starting work, classify the issue as **in-scope** (a bug fix, a type/annotation or docs
59+
correction, or a small self-contained enhancement — the kind of change that lands in roughly one
60+
focused PR touching a bounded set of files) vs. a **large feature request**.
61+
62+
Treat it as a **large feature request** — and STOP — when any of these hold:
63+
- it asks for a **new public API, class, or capability** rather than fixing existing behavior
64+
(e.g. "add support for X", "make the SDK able to Y");
65+
- delivering it would need **design decisions, a new module, cross-package changes, or new
66+
external dependencies**, or would plausibly span multiple PRs;
67+
- it's labeled `type: feature` / `feature request` (or was reclassified as one) and is not a
68+
trivial addition;
69+
- the effort is clearly beyond "quick win" (e.g. the triage doc marks it larger than S), or you
70+
can't scope it to a concrete, bounded change after reading the code.
71+
72+
When it's a large feature request, **do NOT fix it. Backlog it**: report a one-paragraph summary
73+
(what it asks for, rough scope, why it's out of scope for this quick-fix flow) and stop. Do not
74+
create a branch. If a tracking sink is in use (e.g. the Pippin backlog doc), note it there as
75+
"backlogged — large feature request" rather than as a completed fix. When unsure whether
76+
something is a small enhancement or a large feature, ask rather than starting to build it.
77+
78+
### 1c. v2-vs-v3 relevance check (older issues)
79+
Many older issues were filed against **v2** (they cite a v2 version like `1.x`/`2.x`, or use v2
80+
imports such as `sagemaker.estimator`, `sagemaker.tuner`, `sagemaker.pytorch`, `sagemaker.model`,
81+
`sagemaker.workflow.*`). This repo is **v3**. Before fixing such an issue, confirm the bug is
82+
still relevant here:
83+
- **Locate the equivalent v3 code** for the reported behavior (map the v2 symbol to its v3 home
84+
via `AGENTS.md` / `migration.md`; grep the four `src/` trees). The reporter's v2 snippet is a
85+
description of intent — translate it to the v3 API, don't run it verbatim.
86+
- **STOP and report (do not fix)** if:
87+
- the feature was **removed in v3 with no replacement** (e.g. MXNet, Chainer, RLEstimator,
88+
Training Compiler) — nothing to fix here;
89+
- the v3 code path **does not have the bug** — the defect was already fixed or the v3 redesign
90+
doesn't exhibit it. Show the v3 code that proves it's fine.
91+
- **Only proceed** once you've confirmed the defect actually reproduces (or is clearly present by
92+
inspection) in the **v3** code — not merely that it existed in v2. Reproduce against v3 in
93+
step 3, and write the fix and tests against the v3 module.
94+
95+
### 2. Create a working branch
96+
From a clean tree, `git switch -c fix/issue-<n>-<slug>`. If the tree is dirty, stop and report
97+
it rather than mixing unrelated changes. Never work on `master`.
98+
99+
### 3. Reproduce (fork subagent)
100+
Dispatch a **fork** subagent to reproduce the bug. It should write a minimal repro (a script or
101+
a temporary failing test), run it with the PYTHONPATH setup above, and report the exact
102+
observed failure (traceback, wrong value) — or report that it could NOT reproduce, with what it
103+
tried. Do not proceed to a fix until repro is confirmed **or** you've explicitly decided the
104+
issue is valid-by-inspection (e.g. an obvious logic error) and said why.
105+
106+
### 4. Explore the code (Explore subagent)
107+
Dispatch an `Explore` (or fork) subagent to map the code around the failure: the entry point,
108+
call chain to the fault, related tests in `tests/unit/`, and any nearby patterns/conventions the
109+
fix must match. Ask it for `file:line` anchors and the conclusion, not file dumps.
110+
111+
### 5. Write the fix
112+
Edit the source to address the **root cause**, not the symptom. Keep the change focused and
113+
match surrounding style. No hardcoded account IDs, ARNs, regions, or bucket names. Keep it v3.
114+
115+
### 6. Add / update tests
116+
Add or update unit tests under the owning package's `tests/unit/` that **fail without the fix
117+
and pass with it**. Run the relevant subset with the PYTHONPATH setup and paste the real result.
118+
Only consider an integ test (`tests/integ/`) when the bug genuinely needs AWS to exercise; if
119+
so, write it but note it's CI-only and don't require it to pass locally.
120+
121+
### 6b. Validate at the strongest feasible level (before AND after)
122+
A passing mocked unit test is the floor, not proof. Validate the fix as close to the real failure
123+
as you can, and demonstrate the defect **before** the fix and its absence **after**:
124+
- **Reproduce the actual reported error, deterministically and offline, when possible.** If the
125+
bug is about an external contract, exercise that contract directly rather than asserting on
126+
mocks: e.g. validate params against the real boto **service model** with
127+
`botocore.validate.ParamValidator` (offline, no network/credentials); reproduce a type-checker
128+
error by running the real tool (`mypy`) against a translated repro; run the real function for
129+
in-memory logic. State which level you reached and what you could NOT cover.
130+
- **Prove "fails without the fix" against a real baseline.** `git stash push <file>` only stashes
131+
**uncommitted** changes — once the fix is committed on the branch it stashes nothing and you'll
132+
falsely "confirm" a fail. Use `git checkout master -- <file>` as the negative control, then
133+
restore. BUT: if you haven't committed the fix yet, the branch `HEAD` still equals `master`, so
134+
`git checkout HEAD -- <file>` will restore the **unfixed** file and silently wipe your edit.
135+
Safe recipe: **commit the fix first**, then `git checkout master -- <file>` (run the test → it
136+
fails), then `git checkout HEAD -- <file>` (now HEAD has the fix → it passes). After any such
137+
dance, re-grep the file to confirm your fix is actually present before continuing.
138+
- **Never let `|| echo`, `|| true`, or a grep-with-no-match masquerade as success.** Check the
139+
tool's real exit code and read its actual output; a command that errored out is not a pass.
140+
- **Live AWS calls** only when no offline reproduction exists. Prefer ReadOnly/least-privilege,
141+
create only resources you clean up, confirm the account is non-production, and never run
142+
destructive operations. Most SDK bugs (serialization, typing, arg wiring, in-memory logic) are
143+
fully provable offline — reach for a live call last, not first.
144+
- Some things genuinely can't be validated here (e.g. Windows-only behavior with no Windows host,
145+
a real distributed training run). Say so plainly rather than implying coverage you don't have.
146+
147+
### 7. Verify
148+
Run the affected package's relevant unit subset once more and confirm green.
149+
150+
### 8. Independent review (fix-reviewer subagent)
151+
Dispatch the **`fix-reviewer`** subagent (fresh agent, not a fork — it must judge
152+
independently). Give it the issue number/URL and the branch name. It re-derives correctness
153+
from the issue and diff, hunts backwards-incompatibilities (callers/readers, serialized
154+
formats, semantics for existing inputs), and independently reproduces + runs adversarial
155+
checks. Relay its verdict. If it returns `REQUEST CHANGES` with a BLOCKER/MAJOR finding,
156+
address it and re-run the reviewer before summarizing.
157+
158+
### 9. Summarize
159+
Report:
160+
- **Root cause** — what was actually wrong, with `file:line`.
161+
- **Fix** — what changed and why it's correct.
162+
- **Tests** — what you added and the pass/fail output (verbatim if anything failed).
163+
- **Review** — the `fix-reviewer` verdict and any findings you actioned.
164+
- **Branch** — the branch name and `git diff --stat`.
165+
- **Next step** — that it's ready for human review; do NOT push or open a PR.
166+
167+
## Constraints
168+
- You MUST stop at the local branch. Do NOT `git push`, `gh pr create`, or otherwise publish.
169+
- You MUST report test failures faithfully, with output — never claim green without running.
170+
- You MUST NOT introduce v2 patterns; self-check the diff against `AGENTS.md` before summarizing.
171+
- If reproduction fails, say so plainly and ask how to proceed instead of guessing a fix.

‎sagemaker-train/src/sagemaker/train/tuner.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1424,10 +1424,12 @@ def _build_training_job_definition(self, inputs):
14241424
# List of InputData or Channel objects
14251425
for inp in inputs:
14261426
if isinstance(inp, InputData):
1427-
# Convert InputData to Channel
1427+
# Convert InputData to Channel. Preserve content_type so built-in
1428+
# algorithms (e.g. XGBoost) know the data format (issue #5632).
14281429
input_data_config.append(
14291430
Channel(
14301431
channel_name=inp.channel_name,
1432+
content_type=inp.content_type,
14311433
data_source=DataSource(
14321434
s3_data_source=S3DataSource(
14331435
s3_data_type="S3Prefix",

‎sagemaker-train/tests/unit/train/test_tuner.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,35 @@ def test_build_training_job_definition_includes_internal_channels(self):
581581
assert "validation" in channel_names, "User 'validation' channel should be included"
582582
assert len(channel_names) == 4, "Should have exactly 4 channels"
583583

584+
def test_build_training_job_definition_preserves_content_type(self):
585+
"""Regression for #5632.
586+
587+
Converting an InputData to a Channel must carry over content_type, otherwise built-in
588+
algorithms fail because the container doesn't know the data format.
589+
"""
590+
from sagemaker.core.training.configs import InputData
591+
592+
tuner = HyperparameterTuner(
593+
model_trainer=_create_mock_model_trainer(),
594+
objective_metric_name="validation:auc",
595+
hyperparameter_ranges=_create_single_hp_range(),
596+
)
597+
598+
definition = tuner._build_training_job_definition(
599+
[
600+
InputData(
601+
channel_name="train",
602+
data_source="s3://bucket/train/train.csv",
603+
content_type="csv",
604+
)
605+
]
606+
)
607+
608+
train_channel = next(
609+
ch for ch in definition.input_data_config if ch.channel_name == "train"
610+
)
611+
assert train_channel.content_type == "csv"
612+
584613
def test_build_training_job_definition_includes_spot_params(self):
585614
"""Test that _build_training_job_definition includes spot parameters."""
586615
tuner = HyperparameterTuner(

0 commit comments

Comments
 (0)