feat(core): add InstancePreferences for multi-instance-type training and processing - #6249
feat(core): add InstancePreferences for multi-instance-type training and processing#6249papriwal wants to merge 2 commits into
Conversation
* feat(instance-preferences): Add Instance Preferences (multi-instance-type) support (#2093) * feat(instance-preferences): add multi-instance-type support to PySDK Add customer-specified Instance Preferences (multi-instance-type) support across sagemaker-core and sagemaker-train, mirroring the additive, backward-compatible SageMaker model change. shapes / model (sagemaker-core): - New InstancePreference shape (instance_type, optional per-preference instance_count, optional training_plan_arns capped at 1 via the new TrainingPlanArnList model shape). - New processing-specific ProcessingInstancePreference / ProcessingInstancePreferenceList shapes (ProcessingInstanceType/Count members, no TrainingPlanArns - per-type training plans are training-only), matching the service model where training and processing preferences are distinct shapes. - ResourceConfig: instance_preferences + output-only selected_instance_type / selected_instance_count. - ProcessingClusterConfig: instance_preferences + selected_*; relax required on instance_type / instance_count (XOR instance_preferences, enforced server-side; matches the GA contract). - Mirror all additions into the bundled service-2.json codegen input (also consumed by the runtime botocore loader). consumers: - Compute (modules + training configs) inherit the field; preserve nested InstancePreference objects in _to_resource_config so an unset per-preference instance_count does not fail re-validation. - Processor: instance_preferences param + emit InstancePreferences into ClusterConfig; mutual-exclusivity with instance_type/instance_count. - AWS Batch: reconstruct instance_preferences onto the rebuilt Compute. - ModelTrainer: guard local mode / training recipes / JumpStart against instance_preferences with clear errors (out of scope for M1). tests: round-trip for both Compute classes, selected_* not sent on create, ProcessingClusterConfig with processing-specific shapes, Processor ClusterConfig emission and mutual exclusivity, and AWS Batch reconstruction. * fix(instance-preferences): defaults must not inject instance_type; add integ + contract tests - TrainDefaults.get_compute no longer injects the default top-level instance_type / instance_count when Compute carries instance_preferences (mutually exclusive server-side; the injected default made every instance-preferences ModelTrainer job fail CreateTrainingJob validation with 'InstanceType cannot be specified with InstancePreferences' - caught by the new integ test against a live endpoint). Unit tests added for both count modes. - New integ test (tests/integ/train/test_instance_preferences.py): launches a real training job via ModelTrainer + Compute with an ordered instance_preferences list, asserts the create/describe contract, and asserts the resolved selected_instance_type / selected_instance_count is one of the submitted preferences once the job leaves PENDING. Env-gated (role/image/S3 via INSTANCE_PREFERENCES_TEST_* vars); reports XFAIL with the job name while the server-side scheduling stage is not yet deployed on the target endpoint, and enforces the full contract automatically once it is. - New unit test pinning the Instance Preferences contract in the bundled service-2.json (distinct training/processing preference shapes, TrainingPlanArnList cap 1, list cap 5, Selected* on both Describe shapes, ProcessingClusterConfig required relaxation). * fix(instance-preferences): correct count semantics + client validation; add processing integ test Address review feedback on the Processor mutual-exclusivity check: instance_count is NOT flatly exclusive with instance_preferences - it is the shared uniform count for whichever preference wins. Corrected to the count-mode contract (V4) on the Processor AND, for parity, added the same client-side validation to both training Compute classes (server remains the source of truth): - uniform mode: top-level instance_count set, no element carries a count (Processor emits the uniform count into ClusterConfig); - per-preference mode: EVERY element carries a count, top-level unset; - reject both-set, partial per-preference, and neither; - instance_type / instance_groups / instance_placement_config remain mutually exclusive with instance_preferences (V1/V5). Shared validate_instance_preferences helper in modules/utils.py and training/utils.py (per-package twin pattern); wired into both Compute _model_validator hooks (assignment-safe: no-op without preferences). Unit tests for all modes on Processor and both Compute classes; two pre-existing test fixtures corrected to V4-valid combinations. Add processing integ test (tests/integ/processing/test_instance_preferences_processing.py): launches a real processing job via Processor with InstancePreferences through the bundled service model, asserts the create/describe contract and the resolved Selected* winner; env-gated (PROCESSING_INSTANCE_PREFERENCES_TEST_* vars). Reports XFAIL while the endpoint's public model still hard-requires InstanceType/InstanceCount (the @required relaxation ships with the GA ungating), and enforces the full contract automatically once deployed. * revert(instance-preferences): drop AWS Batch reconstruction support Remove the instance_preferences reconstruction in the AWS Batch TrainingQueuedJob path (and its test) - Batch support for Instance Preferences will ship separately later; this PR intentionally adds no Batch functionality. Files restored to upstream/master state. Also: black-format the new training integ test (line-length 100). * fix(instance-preferences): complete client validation; clearer error wording Complete the client-side validation sweep against the server rule set (server remains the source of truth): - Training plans: the top-level (whole-job) training_plan_arn is mutually exclusive with per-preference training_plan_arns (was missing - review catch). - Duplicate instance types across preferences rejected (both Compute classes + Processor). - List-size limits (max 5 preferences, max 1 plan per preference) are deliberately NOT enforced client-side: they are server-side configurable, so raising them must never require an SDK release (botocore does not enforce the C2J list min/max either - verified - so the service is the single enforcement point for sizes). The current limits are documented on the service-2.json list shapes (InstancePreferenceList, TrainingPlanArnList, ProcessingInstancePreferenceList) instead. Type validity (region-dependent) and count bounds are likewise server-only. Reword thrown error messages to use 'top-level instance_count / training_plan_arn' instead of the design-doc jargon 'uniform', so the error names the actual field the customer must change. Tests: whole-job-plan XOR per-preference-plans (reject) / whole-job-plan-without-per-preference-plans (valid) / duplicate-type cases on both Compute classes and Processor. * docs(instance-preferences): add example notebook; extract Processor validation helper Address review feedback: - Add v3-examples/training-examples/instance-preferences-example.ipynb demonstrating the customer experience end to end: ModelTrainer + Compute with an ordered preference list (shared top-level count), per-preference instance counts, per-preference training plans with an on-demand fallback, reading the resolved winner via selected_instance_type/selected_instance_count on Describe, and the Processor equivalent for processing jobs. All construction patterns verified executable against the SDK. - Extract the Processor's inline instance_preferences validation into a module-level _validate_processing_instance_preferences helper (behavior unchanged; unit tests pass unmodified). * docs(instance-preferences): add per-preference-count Processor example to notebook * docs(instance-preferences): polish example notebook for public customers Align the notebook with the v3-examples conventions: - Session()/get_execution_role()/image_uris.retrieve() setup instead of paste-in placeholders; 'Step N' section headers. - Customer-facing wording: drop client/service enforcement internals; 'instance types must be unique across preferences'. (cherry picked from commit 81758d391bb48aa4676bd59def97fef821fe05f5) * fix(instance-preferences): shape-DAG regen, Spark processor plumbing, integ test (#2099) * test(instance-preferences): XFAIL training integ test under pre-GA dark-launch gating The endpoint's frontend strips internal-gated fields (instance_preferences, selected_*) from customer Describe responses while the surface is dark-launched - the documented pre-GA contract (verified: the long-internal UseReservedCapacity is stripped identically, and the layer below returns the fields correctly with the top-level suppression rule applied). Detect the gating (echo absent from Describe) and report XFAIL with a re-run-after- GA-ungate message instead of failing on the unassertable public contract. Verified live against a pre-GA endpoint: job schedules and completes with the correct winner persisted; test XFAILs cleanly in ~2s. * feat(instance-preferences): plumb through Script/Spark processors ScriptProcessor, _SparkProcessorBase, PySparkProcessor, and SparkJarProcessor now accept instance_preferences and forward it to the base Processor (which already carried the parameter, validation, and ClusterConfig emission). Spark image-URI resolution falls back to the first preference's instance type when no top-level instance_type is set. Unit tests: forwarding on all three public classes, validation reuse, and first-preference image lookup (5 new; full processing suite 99 passing). * fix(instance-preferences): regenerate shape DAG; fix integ wait loop The resource deserializer (codec.transform) walks the generated SHAPE_DAG, which was not regenerated when the InstancePreferences shapes landed - so DescribeTrainingJob/DescribeProcessingJob responses silently DROPPED instance_preferences and Selected* from the typed resources (requests were unaffected; the gap was masked while the beta frontend stripped the fields pre-ungate). Regenerated via ShapesExtractor from the bundled service-2.json (+44 lines, additive). Also fix the integ test's wait loop: gate on terminal-status-or-winner instead of a secondary-status allowlist (Selected* propagation lags the transitions; jobs completing fast broke out pre-resolution). Verified live against ungated beta PDX: full contract PASS in 62s (winner + echo + suppression on the typed resource). * fix(instance-preferences): Spark image resolution requires candidate agreement A job runs one image but the winner among instance_preferences is unknown at create, so resolving from the first preference was silently wrong for candidate lists that mix processor classes. Auto-resolution now resolves per candidate and requires all candidates to yield the SAME image; on divergence it raises with a clear pass-image_uri- explicitly message. No behavior change for homogeneous lists (Spark images are CPU-only today, so real lists agree). * fix(instance-preferences): guard degenerate preference list in Spark image resolution An instance_preferences list with no InstanceType on any element hit StopIteration inside the candidate-agreement resolution (which runs before the base Processor validation). Fall through to the classic resolution path so the base validation owns the clean reject. * fix(instance-preferences): name unresolvable Spark candidates Per-candidate image resolution failures (e.g. GPU candidates: Spark has no gpu image variant, so image_uris.retrieve raises) now surface a message naming the candidate and the pass-image_uri-explicitly remedy. Verified unmocked: mixed CPU families (m5/r5/c5) resolve to the same image and pass agreement; a g5 candidate raises the named error; explicit image_uri bypasses resolution entirely. (cherry picked from commit 574a87d540c26d651d4844fc99737f0e1bc60047) * docs(instance-preferences): document instance preferences for training and processing (#2142) * docs(instance-preferences): document training instance preferences The example notebook shipped in #2093 was referenced by no .rst, so Sphinx never rendered it. Add an Instance Preferences section to the training guide (ordered candidates, per-candidate counts and training plans, reading the selected type) and wire the notebook into the Training Examples toctree. Move the notebook's processing steps out to a dedicated processing example so the training guide stays training-only. * docs(instance-preferences): document processing instance preferences Add instance preferences to the MLOps Processing Jobs guide (dict-shaped candidates, shared vs per-candidate counts, reading the selected type, supported processor classes) with a standalone example notebook split out of the training notebook, wired into the MLOps toctree. * docs(instance-preferences): use canonical imports, tighten count-mode rules - Import from sagemaker.core.training.configs instead of the deprecated sagemaker.train.configs shim (which warns on use), and from sagemaker.core.shapes rather than the nested module path, matching the short form used elsewhere in the docs. - State the count rule as it is enforced: exactly one of a top-level instance_count or an instance_count on every candidate; mixed, partial, and omitted are rejected. - Read the winner off the trainer's job resource instead of a redundant TrainingJob.get round trip. (cherry picked from commit 093b2f005e93cb7e0b7eb5e3c2c4fcc721eda07d) * docs(instance-preferences): align with launch drafts, tighten wording (#2146) Corrects and completes the docs against the API and dev-guide drafts: - managed spot training belongs in the mutual-exclusion list - a candidate's training plan must match that candidate's instance type, and the job-level training_plan_arn applies to whichever candidate matches its type (rather than being unusable with a list) - while nothing has capacity the job stays pending and retries the list; max_pending_time_in_seconds bounds the total, not each candidate, and only applies when the list includes an accelerated type - selection is capacity-based and does not validate workload fit - exactly one type is selected; job-level storage and keep-alive settings apply to it, as does billing - per-candidate training plans are training-only, so that limit does not belong in the processing notes Adds the SDK-level restrictions the public page omitted: local mode, training recipes, JumpStart models, and AWS Batch training queues. The Batch restriction is also noted in the AWS Batch section, where a reader choosing queued submission would look for it. Also trims the notebooks: the intro's table of contents duplicated the step headings (and still listed the processing step that moved to its own notebook), and its usage rules restated what each step and the notes already cover. Rules now live once, at the point of use. (cherry picked from commit b720fa83f03b806828c8a58bd40d95ee10279c3a) * fix(model): lower MaxPendingTimeInSeconds bound to 1800 The bundled service model still carried min=7200. Integer bounds are validated from this model before a request leaves the SDK, so the service's lower bound of 1800 was rejected client-side. One line; max and the field documentation are unchanged. (cherry picked from commit 421478b30cbcfcd4c9e3e41c359641f535298845)
#2158) Instance Preferences is publicly released, so the integ tests no longer need the pre-release escape hatches: the XFAIL branches for gated Describe responses, model @required rejections, and InternalServerError before type resolution are removed, along with the rollout notes that explained them. Both tests now assert the full contract unconditionally: create accepted, preferences echoed, top-level instance type absent, winner among the submitted preferences with the expected count. The processing test used a private loader for the bundled service model; the public botocore model now carries the shapes, so it uses a plain boto3 client. Candidate instance types are overridable via INSTANCE_PREFERENCES_TEST_INSTANCE_TYPES / PROCESSING_INSTANCE_PREFERENCES_TEST_INSTANCE_TYPES so the tests can run in accounts where the default types are quota-saturated. Verified against prod us-west-2: processing passes (winner resolved, 3 min); training passes (winner resolved, 8 min).
|
Claude finished @papriwal's task in 1m 55s —— View job PR Review:
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #6249 +/- ##
==========================================
+ Coverage 70.92% 71.00% +0.07%
==========================================
Files 553 553
Lines 68089 68203 +114
==========================================
+ Hits 48295 48425 +130
+ Misses 19794 19778 -16 🚀 New features to boost your workflow:
|
| WAIT_TIMEOUT_SECONDS = 30 * 60 | ||
| POLL_SECONDS = 30 | ||
|
|
||
| pytestmark = pytest.mark.skipif( |
There was a problem hiding this comment.
Could you confirm how the required role and image variables will be provided in the standard CI environment? As written, this module-level skipif causes test_processor_instance_preferences_e2e to be skipped whenever either variable is absent, so an integration run can be green without exercising the new processing path.
If this test requires dedicated resources, please coordinate with the maintainers to wire these variables into the canonical CI configuration while keeping concrete account-specific values out of the public repository. Alternatively, the test can derive suitable defaults from existing fixtures. Before merge, please provide CI output confirming that this test executed and passed rather than being skipped.
There was a problem hiding this comment.
Good catch, the test was written before GA so it was gated like this. Posting fix PR to make it use existing CI resources
| WAIT_TIMEOUT_SECONDS = 30 * 60 | ||
| POLL_SECONDS = 30 | ||
|
|
||
| pytestmark = pytest.mark.skipif( |
There was a problem hiding this comment.
Could you also confirm how the required role, image, and S3 output variables will be supplied for this test in the standard CI environment? Without all three, the module-level marker skips test_model_trainer_instance_preferences_e2e, allowing the suite to pass without validating the new training path.
If these inputs depend on dedicated resources, please coordinate with the maintainers to configure them outside the public source tree. Otherwise, please derive defaults from the existing integration-test setup. Before merge, please provide CI output showing that this test ran and passed rather than being skipped.
There was a problem hiding this comment.
Good catch, the test was written before GA so it was gated like this. Posting fix PR to make it use existing CI resources
Description
Adds support for
InstancePreferences, an ordered list of candidate instance types for training and processing jobs. When set, the service tries each candidate in list order and launches on the first type with available capacity, then reports the resolved choice back viaSelectedInstanceTypeandSelectedInstanceCount.This makes capacity-constrained jobs easier to schedule: instead of picking one instance type and retrying on
InsufficientCapacityError, you supply a ranked list once.Training
instance_preferencesis mutually exclusive withinstance_type,instance_groupsandinstance_placement_config. Each entry may carry its owninstance_count, or you can leave them unset and use the uniformResourceConfig.InstanceCountfor whichever type wins.training_plan_arnsis training-only and capped at one entry per preference.Processing
ProcessingClusterConfiggains the same three members.InstanceTypeandInstanceCountare no longer required on that shape, since a preferences-only config supplies them per candidate —VolumeSizeInGBremains the only required member.What's in this change
sagemaker-coresrcshapes.py,shape_dag.pyand the bundled service modelsagemaker-coretestssagemaker-trainmodel_trainer.pyvalidation,defaults.pyhandling, unit and integ testsdocs/training/index.rst,docs/ml_ops/index.rstGenerated artifacts were produced by
sagemaker-core's own codegen from the bundled sample model, not hand-edited. New shapes:InstancePreference,InstancePreferenceList,ProcessingInstancePreference,ProcessingInstancePreferenceList,TrainingPlanArnList.Unsupported combinations are rejected with a clear error rather than failing at the service: local mode, training recipes, and JumpStart training all reject
instance_preferencesup front.Notes for reviewers
Requires botocore ≥ 1.43.90. These shapes first ship in that release. On an older botocore the client rejects the new members with a
ParamValidationError; only callers using this feature are affected. The dependency floor is bumped as part of the release, not in this PR.One unrelated model line moves. The bundled
service-2.jsonrefresh also bringsMaxPendingTimeInSeconds.minfrom7200to1800, matching botocore 1.43.90+. It shows up in the diff but is not part of this feature — the checked-in model was simply behind on that field.IntPipeVaron five members is not reproducible by codegen.shapes.pywidensinstance_countonInstancePreference,ProcessingInstancePreferenceandProcessingClusterConfig, plusselected_instance_countonResourceConfigandProcessingClusterConfig, toIntPipeVarso pipeline variables can be passed.PIPE_VAR_OVERRIDESintools/constants.pyis keyed by shape name and currently only coversResourceConfig, so re-running codegen narrows these back toOptional[int]. Happy to add the missing override entries here or in a follow-up — worth deciding, since the periodic botocore sync regenerates this file. The twoselected_instance_countfields are Describe-response only, so a pipeline variable arguably shouldn't apply to them at all.Testing
SelectedInstanceType/SelectedInstanceCount; they skip unless the required role, image and output-path environment variables are setIssue #, if available:
Description of changes:
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.