Skip to content

fix: emit boto-style tag keys in ModelTrainer pipeline requests - #6485

Merged
mohamedzeidan2021 merged 1 commit into
aws:masterfrom
mohamedzeidan2021:fix/modeltrainer-lowercase-tag-keys-in-pipeline-definition-6474
Oct 16, 2026
Merged

mohamedzeidan2021 merged 1 commit into
aws:masterfrom
mohamedzeidan2021:fix/modeltrainer-lowercase-tag-keys-in-pipeline-definition-6474

Conversation

@mohamedzeidan2021

Copy link
Copy Markdown
Collaborator

Issue #, if available: Fixes #6474

Description

ModelTrainer emitted invalid lowercase tag keys into pipeline definitions, and CreatePipeline rejected them with Unable to parse pipeline definition. Unknown Argument member 'key'.

_create_training_job_args() called model_dump() on each tag, which yields the model's own field names (key/value — the v3 Tag shape has no aliases). serialize() renames the fields of Tag models to the boto-style {"Key": ..., "Value": ...} form the service requires, but passes plain dicts through untouched:

serialize({"Tags": [Tag(key="k", value="v")]})     # {'Tags': [{'Key': 'k', 'Value': 'v'}]}
serialize({"Tags": [{"key": "k", "value": "v"}]})  # {'Tags': [{'key': 'k', 'value': 'v'}]}

So the dump happened one step too early and the casing was never fixed. This change hands the validated Tag objects to the branch that serializes the request.

The same branch builds the boto3=True payload that TrainingQueue submits to AWS Batch (sagemaker-train/src/sagemaker/train/aws_batch/training_queue.py), so Batch-submitted training jobs carried the same invalid tags; this fixes both.

The interactive request deliberately keeps the dicts. TrainingJob.create annotates its tags parameter with sagemaker.core.resources.Tag — a resource class that shadows the shape of the same name, since resources.py does from sagemaker.core.shapes import * and then redefines Tag. Job.create, defined earlier in the module, still resolves to shapes.Tag:

Job.create          -> Optional[List[sagemaker.core.shapes.shapes.Tag]]
TrainingJob.create  -> Optional[List[sagemaker.core.resources.Tag]]

A shapes.Tag instance therefore fails TrainingJob.create's validation (Input should be a valid dictionary or instance of Tag … input_type=Tag), while the dicts are revalidated into that model and serialized correctly. That is why this fix is scoped to the serializing branch rather than replacing tags_as_dicts with self.tags everywhere, which breaks interactive training. The duplicate Tag class looks worth addressing separately.

self.tags is safe to pass here: ModelTrainer sets validate_assignment=True on an Optional[List[Tag]] field, so it is always None or a list of validated Tag models.

Testing

Added 5 tests to sagemaker-train/tests/unit/train/test_model_trainer.py: the pipeline request uses Key/Value, the boto3=True payload likewise, an untagged trainer adds no Tags member, the interactive request keeps the dicts TrainingJob.create accepts and they arrive boto-style on the wire (measured through a mocked boto client, not derived), and both paths emit identical tags.

With the fix reverted, exactly the 3 tests that assert boto-style keys fail; the other 2 pin behavior this change deliberately leaves alone.

Beyond the unit tests, I validated the step's entire Arguments dict against botocore's real CreateTrainingJob input shape — the same contract the service parses a pipeline definition against:

pipeline step Arguments interactive request
before INVALID — Missing required parameter in Tags[0]: "Key" VALID
after VALID VALID

Running the reproduction script from the issue now prints [{"Key": "project", "Value": "pipeline-tag-reproduction"}].

sagemaker-train/tests/unit/train/test_model_trainer.py ....... 100 passed
sagemaker-mlops/tests/unit/workflow ......................... 359 passed
sagemaker-train/tests/unit .................................. 3451 passed, 19 failed, 21 skipped

The 19 failures are identical with the fix reverted and are environmental in my sandbox, not regressions: 14 require a botocore that describes the SageMaker training-session operations, and 5 call live AWS (DescribeHubContent).

black --check, flake8 and pydocstyle are clean on both changed files; pylint --rcfile=.pylintrc reports the same 11 pre-existing findings and the same 9.81/10 score before and after.

I was not able to run a live CreatePipeline/upsert round-trip (no credentials in my environment), so that last confirmation is outstanding.

Merge Checklist

Put an x in the boxes that apply.

General

  • I have read the CONTRIBUTING doc
  • I certify that the changes I'm introducing will be backward compatible, and I have discussed concerns about this, if any, with the Python SDK team
  • I used the commit message format described in CONTRIBUTING
  • I have passed the region in to all S3 and STS clients that I've initialized as part of this change
  • I have updated any necessary documentation, including READMEs and API docs (if appropriate)

Tests

  • I have added tests that prove my fix is effective or that my feature works (if appropriate)
  • I have added unit and/or integration tests as appropriate to ensure backward compatibility of the changes
  • I have checked that my tests are not configured for a specific region or account (if appropriate)
  • I have used unique_name_from_base to create resource names in integ tests (if appropriate)

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

_create_training_job_args() called model_dump() on each Tag, which yields the
model's own lowercase field names. serialize() renames the fields of Tag models
to the boto-style {"Key": ..., "Value": ...} form the service requires, but
passes plain dicts through untouched, so the dump happened too early and the
pipeline definition kept lowercase keys:

    "Tags": [{"key": "project", "value": "ml-project"}]

CreatePipeline rejected the definition with "Unable to parse pipeline
definition. Unknown Argument member 'key'". The boto3 payload that
TrainingQueue submits to AWS Batch carried the same invalid tags.

Hand the validated Tag objects to the branch that serializes the request. The
interactive request keeps the dicts, because TrainingJob.create annotates its
tags parameter with sagemaker.core.resources.Tag, a resource class that shadows
the shape of the same name, so a sagemaker.core.shapes.Tag instance fails its
validation while the dicts are revalidated into it.

Fixes aws#6474

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI code review

This PR fixes ModelTrainer._create_training_job_args so that tags in the boto3/PipelineSession serialization branch are handed the validated Tag model objects (self.tags) rather than the pre-dumped snake_case dicts. Since serialize/_serialize_shape only renames field names (snake→Pascal) for shape instances and leaves plain dicts untouched, this is what produces the required {"Key": ..., "Value": ...} casing that CreatePipeline and AWS Batch expect. I verified Tag resolves to shapes.Tag with key/value fields, that _serialize_shape performs the PascalCase conversion, that the non-boto3 interactive path still uses the dicts, and that the new tests' imports/constants all exist in the base checkout. The change is correct, minimal, well-tested, and consistent with prior precedent (PR #6415). No issues found.

Reviewed commit 178a161. Automated review; verify before acting.

@mohamedzeidan2021
mohamedzeidan2021 merged commit b04efc3 into aws:master Oct 16, 2026
22 of 27 checks passed

This branch was successfully deployed

1 active deployment
auto-approve — 178a161e Deployed Oct 15, 2026 by mohamedzeidan2021 via wait-for-approval #676
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.

ModelTrainer emits invalid lowercase tag keys in pipeline definitions

2 participants