Skip to content

fix: Keep AsyncPredictor serializers in sync with wrapped predictor - #6334

Merged
lucasjia-aws merged 1 commit into
aws:master-v2from
lucasjia-aws:fix/3100-async-predictor-serde-v2
Sep 29, 2026
Merged

lucasjia-aws merged 1 commit into
aws:master-v2from
lucasjia-aws:fix/3100-async-predictor-serde-v2

Conversation

@lucasjia-aws

@lucasjia-aws lucasjia-aws commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Issue

Fixes #3100. V3 counterpart: #6335

Description

AsyncPredictor exposes serializer and deserializer attributes, but setting them after the predictor is created only partially works. The request payload is serialized with AsyncPredictor.serializer, while the Accept header (self.predictor.accept) and the decoding of the S3 output (self.predictor._handle_response) use the wrapped Predictor's deserializer. As a result, async_predictor.deserializer = JSONDeserializer() is silently ignored and the default deserializer and Accept value are still used. The reverse also holds: a serializer set on the wrapped predictor is not used for the upload.

Root cause

AsyncPredictor.__init__ copies predictor.serializer and predictor.deserializer onto the async predictor, creating two independent copies. Different code paths then read from different copies.

Changes

  • Replace the copied attributes in src/sagemaker/predictor_async.py with serializer / deserializer properties that read from and write to the wrapped Predictor.
  • Remove the attribute copies from __init__.
  • The existing workaround of setting the values on both the async and the inner predictor keeps working.

Testing

  • Added unit tests in tests/unit/test_predictor_async.py covering: a deserializer override drives both the Accept header and result decoding; a serializer override drives the S3 upload body and ContentType; values set on the wrapped predictor are reflected by the async predictor.
  • The new tests fail without the fix and pass with it.
  • tests/unit/test_predictor_async.py, tests/unit/sagemaker/async_inference/, tests/unit/test_expected_bucket_owner.py, tests/unit/sagemaker/model/test_deploy.py: 103 passed.
  • flake8, pydocstyle, and pylint are clean on the changed files.

AsyncPredictor copied the wrapped Predictor's serializer and
deserializer onto itself at construction time. Requests were then
serialized with the outer copy, while the Accept header and response
decoding used the inner predictor's deserializer. Overriding
AsyncPredictor.deserializer after creation therefore had no effect.

Make serializer and deserializer properties that read from and write
to the wrapped Predictor, so a single source of truth drives the
upload, the Accept header, and result decoding.

Fixes aws#3100
@lucasjia-aws
lucasjia-aws merged commit f25e36f into aws:master-v2 Sep 29, 2026
9 of 11 checks passed

@mohamedzeidan2021 mohamedzeidan2021 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. Reproduced the issue exactly: on master the serializer override works for the S3 upload but the deserializer is silently ignored — Accept: */* instead of application/json, and the result comes back as raw bytes. Clean with the fix. The property delegation is what the issue author recommended, and it's the root cause rather than a mask.

Checked every writer: model.py, multidatamodel.py and pipeline.py all set serializers on the inner predictor before wrapping, so existing behaviour is unchanged; jumpstart/factory/model.py hard-raises for non-base-Predictor, so that mutation path never sees an AsyncPredictor. No subclasses anywhere, deepcopy survives, and the documented workaround degrades to a harmless redundant double-write. 3 new tests fail on reverted source; 23 passed in the async suite, 240 across model/predictor/multidatamodel/pipeline.

Best integ evidence in this batch: tests/integ/test_async_inference.py::test_async_walkthrough PASSED against a live async endpoint in this run. The job reads red only because the OIDC session expired at 99% completion, with zero test failures reported.

Non-blocking follow-up: _create_request_args does ", ".join(self.predictor.accept), but Predictor.accept's setter is annotated val: str and stores a plain string — so predictor.accept = "text/csv" renders the header "t, e, x, t, /, c, s, v". Pre-existing and identical on master, so not a regression here, but it's the same "content-type lever silently doesn't work in async" family as #3100 and worth its own issue.

Note this overlaps files with #6337 — whichever lands second will need a rebase and re-run.

This branch was successfully deployed

1 active deployment
auto-approve — a907a85c Deployed Sep 25, 2026 by lucasjia-aws via wait-for-approval #235
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.

3 participants