change(train): gate deep integ tests behind gpu_intensive, add shallow submit-then-stop suite - #6176
change(train): gate deep integ tests behind gpu_intensive, add shallow submit-then-stop suite#6176jam-jee wants to merge 5 commits into
Conversation
…w submit-then-stop suite Replaces the CodeBuild integ suite for sagemaker-train on the PR gate with a faster selection that keeps meaningful server-side coverage. CreateTrainingJob returns a TrainingJobArn only after the request has cleared every synchronous server-side gate: public-model shape validation, SigV4, sagemaker:CreateTrainingJob authorization (including condition keys), iam:PassRole on the execution role, the training backend's synchronous request validators, its role-assuming validators (which make real S3/ECR/FSx calls as the customer), post-validator business logic (training-plan capacity, routing, recipe filtering) and the final conditional write that rejects duplicate job names. So "the ARN came back" proves the SDK-shaped payload was accepted as sent and the caller held the permissions needed to submit it -- without paying for a training run. Adds tests/integ/train/shallow (70 tests) built on that: submit, assert the ARN, stop immediately. Covers ModelTrainer (payload shaping, source-code packaging, input channels, compute, networking, checkpointing/spot), the recipe trainers (SFT/DPO/RLVR/RLAIF, serverless and serverful), recipe customization (overrides, explicit recipe files, sequence_length, DataMixingConfig), and the non-training job types (HyperParameterTuningJob, AgentRFT Job). Includes negative tests so the suite cannot pass merely because some ARN came back. Marks the 19 previously-unmarked tests that submit a job and wait for it with gpu_intensive, so they continue running on the scheduled CI-health workflows instead of the PR gate. Widens that marker's description: despite the name it gates anything consuming real training capacity, including serverless and CPU-instance jobs. The PR job now runs the whole tests/integ/train tree with -m "not gpu_intensive and not us_east_1" rather than only shallow/, which keeps the ~170 client-side tests (recipe resolution, data utils, dry-run, log streaming) on the gate -- they make no service call and were never the expensive part. Net: 191 of 251 tests on the PR gate, none of which waits for a training job. This is a deliberate scope reduction: training *behaviour* (artifacts, metrics, convergence) is no longer asserted on the PR gate. A regression that breaks training itself -- a bad entry script, a broken container command -- will pass here and be caught by the scheduled suites.
…ter first real AWS run Verified against AWS in account 729646638167 (us-west-2): * test_unassumable_role_is_rejected: ModelTrainer.__init__ validates the role via iam:SimulatePrincipalPolicy, so a bad role raises RoleValidationError at construction and never reaches CreateTrainingJob. Assert around the constructor instead of around train(). * test_cpt_trainer_is_accepted: CPTTrainer takes no training_type, and its compute is HyperPodCompute-only, so it cannot use the shared _trainer helper. WIP: 2 further real failures still to fix (RLAIF compute, tuner job-name collision). See SHALLOW_TEST_RUN_STATE.md.
Ran the suite against account 729646638167 (us-west-2) with PYTHONPATH pointed at
this clone, and fixed every failure it surfaced. All were wrong assumptions in
the tests, not service problems:
* conftest: add a session-scoped bundled_service_model fixture setting
AWS_DATA_PATH to sagemaker-core/sample. The public botocore model has no
ServerlessJobConfig.SequenceLength, so sequence_length requests were rejected
client-side before reaching the service. Mirrors the existing
setup_aws_data_path fixture in test_recipe_override_integration.py.
* harness: unique_name() now takes max_length. Tuning job names are capped at 32
characters, not the 63 allowed for training jobs, and the service enforces it:
Value '...' at 'hyperParameterTuningJobName' failed to satisfy constraint:
Member must have length less than or equal to 32
* tuner tests: submit under an explicit job_name via a _tuning() context manager.
The tuner derives its default name from the training image plus a
second-granularity timestamp and ignores base_job_name, so two tuner tests in
the same second collided with ResourceInUse.
* RLAIF: excluded from TestServerfulSubmission. RLAIFTrainer has no compute
parameter, so it has no serverful path. Still covered by every serverless case.
* CPT: marked gpu_intensive and skipped unless SHALLOW_HYPERPOD_CLUSTER is set.
CPT refuses to submit without HyperPod compute, and HyperPod targets a
pre-provisioned cluster rather than CreateTrainingJob.
* sequence_length / training_type: narrowed to the values the recipe catalogue
actually offers for this model ('4K' only; no serverless recipe for FULL).
Both left parametrized so more values can be added against a model that
supports them, rather than dropping the distinction.
Result: 62 passed, 0 failed, 5m18s serial (~5s/test).
Cost model confirmed empirically rather than assumed: across 100 jobs created by
these runs, every one ended Stopped and every BillableTimeInSeconds was null.
Jobs are torn down while still in Starting/Pending, before instances become
billable.
|
Claude finished @jam-jee's task in 2m 51s —— View job ReviewThorough, well-documented PR. The rationale (submit-then-stop as a cheap proxy for "the service accepted this payload and the caller was authorized") is sound, the negative tests are the right instinct, and the README/docstrings are unusually good. Comments below, none blocking.
1. Security / isolation change worth flagging to approvers —
|
…t have shallow coverage
Addresses two review points.
1. Only mark deep tests that this suite actually replaces.
Reverts gpu_intensive from 9 tests that had no shallow counterpart, so the PR gate
no longer loses coverage with nothing replacing it:
* all 8 evaluator tests (benchmark, custom scorer, inspect_ai, llm_as_judge x2,
llmaj_custom_model) -- evaluate() is a different API surface returning pipeline
executions, and this suite has no coverage for it
* test_notifications.py -- asserts EventBridge/SNS side effects, not submission
10 marks remain, each with a named shallow equivalent documented in the suite
README. The rule is written down there: do not mark a deep test unless a shallow
test covers the same path.
2. One file per trainer, matching the existing deep-suite layout.
test_recipe_trainers_submission.py -> test_{sft,dpo,rlvr,rlaif,cpt}_trainer.py
test_recipe_customization_submission.py (recipe cases folded into rlvr/sft;
Nova data mixing to its own file)
test_other_job_types_submission.py -> test_tuner.py,
test_multi_turn_rl_trainer.py
test_model_trainer_submission.py -> test_model_trainer.py
The "recipe_*" names described how the SDK groups these internally rather than
what a reader looks for; the shallow counterpart of a given deep test is now
obvious from the filename.
recipe_cases.py holds the cases every recipe trainer shares. Each per-trainer
class subclasses RecipeTrainerCases and sets TRAINER, so a new trainer is a
two-line file, and per-trainer deviations are declared rather than duplicated:
EXTRA_KWARGS (RLAIF's reward model), SUPPORTS_SERVERFUL=False (RLAIF takes no
compute), SUPPORTS_TRAINING_TYPE=False (CPT has no LoRA/full split). Not named
test_* so pytest does not collect the base class.
Inheriting the shared cases also widened coverage: DPO and RLAIF now get the full
set (output path, dataset override, both negative cases) rather than only the
three they had as parametrized entries. 80 tests total, 69 on the PR gate.
Verified against AWS (account 729646638167, us-west-2): 68 passed, 1 skipped,
0 failed in 6m59s. The skip is RLAIF's serverful case, reporting
"RLAIFTrainer takes no compute argument".
|
Claude finished @jam-jee's task in 2m 10s —— View job ReviewRe-review after the latest push. The suite has been restructured into one file per trainer since the first automated review, so a few of that review's line references (e.g. Review checklist
1. Trust-boundary change worth calling out to approvers —
|
… has an equivalent
Previous commits only audited the marks this PR added. This audits all 46
gpu_intensive tests in tests/integ/train -- including those already marked on
master -- and adds the missing shallow counterparts.
Added (were gaps):
* MLflow, in RecipeTrainerCases so all four recipe trainers get it. Every
*_complete_workflow deep test configures MLflow, so without this their
shallow counterparts missed that half of the payload. Two forms:
experiment/run names (always runs) and mlflow_resource_arn (skips if the
account has no app).
* RLVR reward functions, all three forms the deep suite covers: hub-content
ARN, Lambda ARN (auto-creates an Evaluator), and a pre-created Evaluator
object.
* RLAIF reward_prompt as a hub-content ARN rather than a Builtin.* name, and
continued fine-tuning from a model-package ARN.
* Nova SFT and Nova RLVR, in test_nova_trainers.py. Nova needs a different
recipe family, region and account, so it cannot share RecipeTrainerCases;
marked us_east_1.
Two real constraints the AWS run surfaced, both now recorded in comments:
* The reward-function tests cannot use this suite's generic chat-format
fixture. Before submitting, the SDK *invokes* the reward function over
sample records and fails if they do not score ("GSM8k scoring failed").
They now use the same dataset as the deep RLVR suite, via a dedicated
reward_scored_data_uri fixture.
* list_mlflow_apps is not a paginatable operation, so the fixture calls it
directly instead of via get_paginator.
Also fixed a ScopeMismatch: the three new lookup fixtures were session-scoped
but depend on the parent conftest's module-scoped sagemaker_session.
All three new fixtures (mlflow_arn, reward_lambda_arn, reward_evaluator) only
look resources up and skip when absent. The deep suite's equivalents create
them -- IAM roles, Lambdas, MLflow apps, registry entries -- which is a durable
side effect a fast PR-gate suite should not have.
Still uncovered, documented in the suite README with the reason: the 11 evaluator
tests (evaluate() is a different API surface returning pipeline executions) and
the 3 HyperPod tests (submit to a pre-provisioned cluster, not
CreateTrainingJob). Neither is newly marked by this PR, so no coverage is lost;
the evaluator gap is the clearest follow-up.
97 tests total, 82 on the PR gate. Verified against AWS (729646638167,
us-west-2): 81 passed, 1 skipped, 0 failed in 7m04s. The skip is RLAIF's
serverful case, which reports its own reason.
|
Claude finished @jam-jee's task in 1m 31s —— View job ReviewRe-reviewed the current tree after the latest push (whole diff read from Checklist
1.
|

What this changes
Replaces the CodeBuild integ suite for
sagemaker-trainon the PR gate with a faster selection that keeps meaningful server-side coverage.CreateTrainingJobreturns aTrainingJobArnonly after the request has cleared every synchronous server-side gate: public-model shape validation, SigV4,sagemaker:CreateTrainingJobauthorization (including condition keys),iam:PassRoleon the execution role, the training backend's synchronous request validators, its role-assuming validators (which make real S3/ECR/FSx calls as the customer), post-validator business logic (training-plan capacity, routing, recipe filtering), and the final conditional write that rejects duplicate job names.So "the ARN came back" proves the SDK-shaped payload was accepted as sent, and the caller held the permissions needed to submit it — without paying for a training run. The job is stopped immediately.
New:
tests/integ/train/shallow(97 tests, 82 on the PR gate)One file per trainer, mirroring the existing deep-suite layout so the shallow counterpart of any deep test is obvious:
test_model_trainer.pytest_model_trainer.pytest_sft_trainer.pytest_sft_trainer_integration.pytest_dpo_trainer.pytest_dpo_trainer_integration.pytest_rlvr_trainer.pytest_rlvr_trainer_integration.pytest_rlaif_trainer.pytest_rlaif_trainer_integration.pytest_cpt_trainer.pytest_cpt_hyperpod.pytest_multi_turn_rl_trainer.pytest_multi_turn_rl_trainer_integration.pytest_tuner.pytest_tuner_distributed.pytest_nova_data_mixing.pytest_sft_trainer_data_mixing_integration.pytest_nova_trainers.py::test_sft_trainer_nova_workflow,::test_rlvr_trainer_nova_workflowharness.pyprovidessubmitted()/assert_submitted()/assert_rejected(): forceswait=False, resolves the submitted job across the four attribute names trainers use for it, and stops the job in afinallyso a failed assertion still cleans up.recipe_cases.pyholds the cases every recipe trainer shares. Each per-trainer class subclassesRecipeTrainerCasesand setsTRAINER, so adding a trainer is a two-line file and per-trainer deviations are declared rather than duplicated —EXTRA_KWARGS(RLAIF's reward model),SUPPORTS_SERVERFUL = False(RLAIF takes nocompute),SUPPORTS_TRAINING_TYPE = False(CPT has no LoRA/full split).Negative tests are included deliberately: without them the suite would stay green even if the SDK started sending a permissive-but-wrong payload.
Not every trainer creates a
TrainingJob—HyperparameterTunercreates aHyperParameterTuningJobandMultiTurnRLTrainercreates an AgentRFTJob— soassert_submittedtakes the expected ARN resource segment.Coverage of every
gpu_intensivetestThe rule: a deep test belongs off the PR gate only if this suite covers the same code path. There are 46
gpu_intensivetests intests/integ/train. All are accounted for:Covered by this suite —
test_model_trainer.py(8: tar source, py/sh entry, MPI, torchrun, HP json/yaml, custom driver), SFT (complete workflow incl. MLflow, validation dataset, sequence length, Nova, serverful SMTJ), DPO (both), RLAIF (complete workflow, reward-prompt ARN, continued fine-tuning), RLVR (complete workflow, all three reward-function forms, recipe+overrides, sequence length, Nova), tuner (sm_driverschannel), MTRL (3, needs prerequisites), CPT HyperPod (needs a cluster), Nova data mixing.The full test-by-test mapping is in
tests/integ/train/shallow/README.md.MLflow is worth calling out: every
*_complete_workflowdeep test configures it, soRecipeTrainerCasescovers both forms — experiment/run names (always runs) andmlflow_resource_arn(skips when the account has no app).Not covered, and why:
test_benchmark_evaluator.py,test_custom_scorer_evaluator.py,test_mtrl_evaluator_3p_agent.py,test_mtrl_trainer_integration.py) —evaluate()is a different API surface returning pipeline executions rather than jobs, so it needs its own harness support. Alreadygpu_intensiveon master, so this PR loses no coverage. Clearest follow-up.CreateTrainingJob.test_cpt_trainer.pyis written in the shallow style and activates whenSHALLOW_HYPERPOD_CLUSTERis set.This PR newly marks only 10 tests: the 8 in
test_model_trainer.py,test_sft_trainer_lora_with_sequence_length, andtest_tuner_includes_sm_drivers_channel. Everything else listed above was already marked on master.Deliberately not marked, because this suite does not cover them: all evaluator tests,
test_notifications.py(EventBridge/SNS side effects),test_local_model_trainer.py(no service call).The rule is documented in the suite README so a future change cannot silently erode the gate.
tox.ini: widened thegpu_intensivedescription. Despite the name it gates anything consuming real training capacity, including serverless and CPU-instance jobs.Fixtures that look up rather than create
mlflow_arn,reward_lambda_arnandreward_evaluatoronly look their resources up and skip when absent. The deep suite's equivalents create them — IAM roles, Lambdas, MLflow apps, registry entries — which is a durable side effect a fast PR-gate suite should not have.Workflow change
The PR job now runs the whole
tests/integ/traintree with-m "not gpu_intensive and not us_east_1"rather than onlyshallow/. That keeps the ~170 client-side tests (recipe resolution, data utils, dry-run, log streaming, docker-compose detection) on the gate — they make no service call and were never the expensive part.Verification
Run against a real account (us-west-2):
The skip is RLAIF's serverful case, which reports its own reason:
RLAIFTrainer takes no compute argument. Tests needing us-east-1 credentials or a HyperPod cluster are deselected on the gate and skip cleanly when their prerequisites are absent.Cost model measured, not assumed. Across 100 training jobs created by these runs, every one ended
Stoppedand everyBillableTimeInSecondswasnull— jobs are torn down while still inStarting/Pending, before instances become billable.Real bugs the AWS runs found in these tests (all wrong assumptions on my part, not service problems), each now fixed with the evidence recorded in a comment:
ServerlessJobConfig.SequenceLength, so those requests failed client-side before reaching the service — fixed with anAWS_DATA_PATHfixture mirroring the one intest_recipe_override_integration.pybase_job_name, so two tuner tests in the same second collided withResourceInUseModelTrainer.__init__validates the execution role, so a bad role is rejected at construction and never reachesCreateTrainingJobRLAIFTrainertakes nocompute;CPTTrainertakes notraining_typeand requires HyperPod4Ksequence length and no serverless recipe forTrainingType.FULLGSM8k scoring failedlist_mlflow_appsis not a paginatable operationWhat this deliberately does NOT cover
Training behaviour: no model artifacts, no metrics, no container logs, no convergence.
A regression that breaks training itself — a bad entry script, a broken container command, a distributed-launch bug — will pass this gate and be caught by the scheduled suites instead. That is the accepted trade for the runtime and cost reduction, and it is stated plainly in
tests/integ/train/shallow/README.md.Also out of scope for this pattern: HyperPod (submits to a pre-provisioned cluster, not
CreateTrainingJob) and local container mode (no service call).Known remaining gap: evaluator
evaluate()submissions. Same pattern applies; it is a distinct API surface, so it is left for a follow-up — and the corresponding deep tests stay on the PR gate until then.Note for reviewers
Because this workflow triggers on
pull_request_target, CI runs the workflow definition from the base branch, not this PR's. So thefast-integ-testsjob added here does not appear in this PR's checks, and the oldinteg-tests (sagemaker-train)job still runs — the workflow change only takes effect once merged. The test selection was verified locally (numbers above); the job definition itself is unproven in CI.