Repository navigation
Stop the generation memory leak behind the 512 MiB OOMs - #38
Merged
Merged
Conversation
openai 2.21.0's responses.parse builds new ParsedResponse[...] classes on every request under uvicorn and caches a TypeAdapter for each in an unbounded lru_cache, which leaked about 1 MiB per uncached generation until Cloud Run killed the instance at 512 MiB. Send the same strict JSON schema format through responses.create and validate the output_text parts with the same rules as responses.parse.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
lp-backend-prodwas killed for exceeding its 512 MiB memory limit 6 times in the last 30 days. Each kill dropped the request in flight, and three of them showed users a 503.The cause is a memory leak in
responses.parse(text_format=...)in the lockedopenai2.21.0:contextvars.Context.ContextVar, so that cache is empty on every request.responses.parsecall builds newParsedResponse[...]classes, and the SDK caches aTypeAdapterfor each one in an unboundedlru_cache.In production, about 1–1.4 MiB stays behind after every uncached generation. Every instance that was killed had served about 320–345 uncached generations, no matter how long it had been running. Upstream tracks this as openai-python#3084. It is fixed in
openai3.16.2, but that is a major-version upgrade (it switches the HTTP client to httpx2), so this PR works around the leak instead.What changes
responses.createand sends the same strict JSON schema format.LEARNING_PATH_TEXT_FORMATis built once at import with the SDK's owntype_to_text_format_param, the helperresponses.parseuses.parse_graph_outputcopies whatresponses.parsedid. It validates everyoutput_textpart withLearningPathGraphOutput.model_validate_jsonand returns the first one, orNonewhen there is none. It runs inside_request_completion's existingtry, so parse errors map to the same errors as before.responses.create.parse_graph_outputcover several parts, one invalid part, and refusal and reasoning items.test_generation_does_not_grow_sdk_type_cache: it runs 20 generations through the real SDK, each in a freshcontextvars.Contextas uvicorn does, and asserts the SDK'sTypeAdaptercache does not grow. Onmainit fails withassert 47 == 7.Moderation, the prompt, the model and reasoning settings, caching and timing are unchanged. The request OpenAI receives is the same, so the timing history version does not need a bump.
Verification
uv run pytestruff format --check,ruff checkmain/ on this branch47 == 7) / passes 10 out of 10 runs, in any test order/v1/responsesrequest body and headers, old vs newgpt-6-lunaand a model overrideresponse.output_parsed→parsed output).Memory under the real server. Setup: single-process uvicorn, Python 3.12, and a local fake OpenAI server (no real API calls). It ran 400 sequential uncached generations, then 5 bursts of 40 concurrent generations.
mainworking setOn this branch the SDK's
TypeAdaptercache stays at 13 entries; onmainit grows by 2 per request. A separate 3,000-request soak on this branch stayed level at about 111 MiB.Not tested here: a generation against the real OpenAI API. The request is byte-identical, so the first prod generations after the deploy will confirm it.
Deploy note
This changes
server/**, so merging deployslp-backend-prodthrough Cloud Build. After the deploy, checkmemory/utilizations. It should stay flat past about 350 uncached generations, where it used to reach the limit.Merge order with #36: the two PRs merge without conflicts, but #36's new test
test_moderation_and_generation_get_cleaned_topic_with_punctuationstill mocksresponses.parseand fails on top of this PR. Whichever PR merges second needs to rebase first. On #36, its two new tests should mockresponses.createwithoutput=[message], as the tests here do.