Skip to content

Avoid retaining dynamic workflow manifests in global caches - #3197

Merged
dkosowski87 merged 3 commits into
mainfrom
codex/avoid-global-dynamic-manifest-cache
Oct 15, 2026
Merged

dkosowski87 merged 3 commits into
mainfrom
codex/avoid-global-dynamic-manifest-cache

Conversation

@dkosowski87

@dkosowski87 dkosowski87 commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

What improves

When a workflow with custom Python blocks has to be compiled again, it creates new Python classes describing those blocks. The compiled-workflow cache eventually evicts old workflows, but separate, process-wide metadata caches can still hold their generated classes alive. Repeated cache misses therefore retain old compilation objects and increase memory use.

#3196 restores reuse of compiled workflows for local custom Python blocks. This follow-up handles what happens when reuse is impossible or an old workflow is evicted: supporting caches no longer keep its generated manifests alive independently. Once the compiled workflow and other users release them, those classes become eligible for garbage collection.

The compiled-workflow cache still holds up to 256 workflows, and static built-in/plugin manifest caching stays enabled. On a compilation miss, dynamic manifest metadata is computed without inserting the generated class into a global supporting cache. A compilation-cache hit still reuses the compiled workflow.

flowchart TD
    A[HTTP workflow request] --> B{Compiled workflow cached?}
    B -->|Yes| C[Reuse compiled workflow]
    B -->|No| D[Generate custom block classes]
    D --> E[Read dynamic manifest metadata without globally caching the classes]
    E --> F[Compile workflow]
    F --> G[Store in existing compiled-workflow cache]
    G --> H[Run workflow]
    C --> H
    G --> I[Eventually evict old compiled workflow]
    I --> J[Release compilation-cache references]
    J --> K[Generated classes can be collected when no other owners remain]
    style E fill:#d9f2df,stroke:#27823b,color:#163d20
Loading

The highlighted step is this PR's change. #3196 reduces how often classes are created; this PR prevents supporting caches from unnecessarily extending their lifetime. Garbage collection makes memory reusable, but Python/native allocators may retain freed pages, so RSS need not immediately fall.

Implementation

  • Mark generated dynamic manifests and bypass the global parsed-input, JSON-schema and output-description caches for those classes.
  • Bypass the workflow validation-model cache when its available blocks include a dynamic manifest.
  • Preserve static cache behavior and inspection/clear methods; leave compilation-cache eligibility and resolver isolation unchanged.
  • Add an engine changelog entry. No workflow migration is required. Investigation scripts and additional test files are excluded from this PR.

Performance observations and tradeoff

We did not observe an overall slowdown with this fix in the recorded experiments. The current implementation keeps the simpler approach: bypass supporting caches for dynamic manifests while preserving compiled-workflow reuse and static manifest caching.

Recorded test Without this fix With this fix
Local compilation/metadata probe: 10,000 iterations including workflow validation-model creation 36.03 s 30.70 s
T4 control: 500 workflow compilations, excluding final cleanup 71.30 s 69.95 s
#3196 HTTP test: unchanged workflow 5.00 requests/s 5.00 requests/s
#3196 HTTP test: forced cache misses 4.64 requests/s 4.91 requests/s*

The first two rows are earlier experiments comparing the original supporting-cache behavior against this fix; they are not measurements of #3196's cache-hit path. The last two rows compare #3196 alone against #3196 plus this fix. Times are elapsed workload times, not isolated per-function measurements.

* The combined run's 4.91 requests/s covers its recorded portion before the connection reset; the baseline completed the full phase. Both HTTP phases targeted 5 requests/s with at most four outstanding requests, so these rates do not establish maximum throughput. These were memory-focused experiments rather than repeated performance benchmarks: the observed results were comparable or faster with the fix, but they do not establish a reliable speedup or rule out overhead in other workloads.

The potential tradeoff is extra CPU work when the same dynamic manifest is inspected repeatedly:

  • Compiled-workflow cache hit: the compiler returns the cached graph before generating classes or building the workflow validation model. This change does not add graph recompilation to that path, preserving the reuse benefit restored by Restore compilation caching for locally executed custom Python blocks #3196.
  • Cache miss or intentionally uncached compilation: JSON schemas, input metadata and output descriptions are recomputed whenever requested for a dynamic manifest. Repeated inspection of the same generated class loses the supporting-cache speedup. Validation models containing dynamic manifests also bypass their supporting cache. The cost depends on manifest complexity and inspection frequency.
  • Across separate compilations: fresh class identities mean old class-keyed supporting-cache entries generally could not accelerate the next compilation anyway.
  • Static manifests: their supporting caches remain enabled, with an additional dynamic-marker check in the decorator.

We have not isolated the cost of those additional computations, so there is no measured percentage slowdown to quote. Reduced retention and garbage-collection costs could offset that work, but the experiments do not establish the cause of the observed timings. Class-owned metadata caching remains a possible future optimization if targeted profiling identifies a meaningful cost; it is not part of this PR.

T4 validation

A synthetic 1700×1028 image, public pretrained YOLO26 Medium model, and an eight-step workflow with three custom Python blocks exercised masking, inference, measurements and annotations over HTTP. External uploads were disabled. Each variant used a fresh process, a target of 5 requests/second and at most four outstanding requests. Both used the same existing GPU image and dependencies with mounted source: PR #3196 at a7da6cafe7ba6d75d77e2307c81d3f1445dc4232, with or without this production patch.

After 4,096 requests with one unchanged definition, the client cycled 512 equivalent definitions by changing only metadata. This exceeded the 256-entry compilation cache and forced repeated misses and eviction. No forced garbage collection, cache clearing or allocator trimming occurred during load.

Measurement #3196 alone #3196 + this fix
RSS after 4,096 unchanged requests 1.70 GiB 1.72 GiB
RSS at 8,064 completed eviction-phase requests 8.47 GiB 2.35 GiB
Supporting manifest-cache entries at that checkpoint 10,000 5 static

Synthetic workflow memory comparison on T4

The last panel measures the supporting manifest cache, not the compiled-workflow cache. The blue line has five static entries, which look almost like zero at this scale. Dynamic manifests still live inside cached compiled workflows.

The combined run's last 2,048 recorded requests stayed within 2.346–2.363 GiB. It encountered one connection reset near the end; the last persisted checkpoint was 8,064 of 8,192 planned eviction-phase responses. The server remained responsive and was not OOM-killed; the reset's cause is unproven. The comparison above uses the same completed-request checkpoint for both variants.

A separate fresh-process confirmation with connection reuse disabled completed 32 warm-up requests plus 4,096 forced misses, with no retries or transport errors. Final RSS was 2.362 GiB and peak RSS was 2.372 GiB. Post-load garbage collection left 768 live generated manifests = 256 cached workflows × 3 blocks, and only five static supporting-cache entries. This confirmation is separate from the plotted traces. Measurements and annotated-image bytes matched across completed phases; the interrupted phase's final image-signature aggregate was unavailable.

Checks

  • Before removing the investigation-only files (production code unchanged): 574 passed, including 15 investigation regression cases, with PYTHONPATH=workflows:inference_models .venv/bin/python -m pytest -q workflows/tests/unit_tests/execution_engine/introspection workflows/tests/unit_tests/execution_engine/compiler/test_dynamic_blocks_compilation_cache.py workflows/tests/unit_tests/execution_engine/compiler/test_cache.py workflows/tests/unit_tests/execution_engine/dynamic_blocs --disable-warnings.
  • T4 runtime: PR compilation-cache tests plus the investigation manifest-cache tests: 30 passed. The investigation test file is not included in this PR.
  • Black and isort checks, critical flake8 checks (E9,F63,F7,F82) on the five production Python files and the two subsequently removed investigation files, and git diff --check: passed.
  • Full repository suite was not run. The plotted results came from the separate HTTP/T4 workload described above; benchmark tooling is not included in this PR.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

👋 Thanks for the pull request! Here is how automated Claude review works here, so you spend credits (and reviewer time) wisely.

🚦 This PR is marked Ready for review, so automated Claude review will run — and every pass spends real credits.

Warning

💸 The Claude reviewer bills in credits, not vibes

Automated review spins up a real agent that reads real code and spends real credits on every pass. It is glad to help — but it is not a rubber duck, a linter you poke in a loop, or a substitute for reading the contributing guide. Treat it like an expensive senior reviewer whose time you booked, and show up prepared.

Draft when unsure, Ready when you mean it:

  • 🌱 Not sure the PR is in good shape yet? Keep it (or set it back) as a draft — drafts pause review, so you can push and iterate without burning credits on a moving target.
  • 💪 Feel strong about the contents? Mark it Ready for review and the reviewer will take a look.

However you get there, arrive prepared:

  • 🧱 Bring a SOLID, thorough PR. Point your local agent at our skills/ to tune it to our guidelines first — or, if you are one of those fabled carbon-based contributors, read them yourself. A half-baked diff costs exactly the same to review as a finished one.
  • ✅ Resolve every comment before you re-request review. Re-requesting with threads still open means paying twice for the same conversation.
  • 🔁 Do not use CI review as an inner loop for a local agent. The reviewer is not a step-by-step debugger — do the unfolding locally and arrive with the answer, not the search.
  • 🙋 If something looks off, ask a human. One question to a maintainer is cheaper and faster than three rounds of agent re-review chasing a misread.

Reviews are not free. A draft costs nothing to review; a Ready PR is a promise that it is worth reviewing.

  • Prefer to skip automated review entirely? Add the skip-claude-review label.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Claude review started at commit f64500d92461d91a32a60e734acf9b843e3885a4.

New commits are not auto-reviewed. Add the claude-review label to request a re-review — the label is consumed when the review starts, so just add it again next time.

@github-actions

Copy link
Copy Markdown
Contributor

Claude review summary

Skills: review-workflows-execution-engine, review-topic-concurrency-and-resource-safety, review-topic-test-hygiene

No high-confidence medium+ findings.

What I checked in the code (I didn't rely on the PR description):

  • One assembly point for dynamic manifests. assembly_dynamic_block_manifest (block_assembler.py) is the only place dynamic manifests are created, and it marks every one with __workflow_dynamic_manifest__. Both the structural and executable compile_dynamic_blocks paths go through it, and the new test is parametrised over both. Static manifests never get the marker, so is_dynamic_manifest falls back to False for them.
  • No hit rate lost. Dynamic manifest and block class names contain a fresh uuid4() on every compilation. WORKFLOW_DEFINITION_ENTITIES_CACHE is keyed by block_source + identifier, so under the old code dynamic entries could never be hit again. The same goes for the class-keyed lru_caches. Skipping those caches gives up nothing and only removes the retention.
  • Static caching is unchanged. cache_static_manifest keeps cache_clear/cache_info/cache_parameters working, so clear_caches()/clear_cache() and the benchmark still work. It handles both positional and keyword calls; every call site uses one of those two forms (selectors_parser, connections_discovery, blocks_loader, workload.py, schema_parser).
  • Tests are isolated. The cache_info() == (1, 1, 1) assertions are safe because the autouse fixture in introspection/conftest.py clears the caches before and after each test. The new tests sit under workflows/tests/unit_tests, which the x86 workflows unit CI runs.
  • Changelog. The entry is under ## Unreleased → ### Execution engine in workflows/CHANGELOG.md.

Minor doubt (non-blocking): during one compilation miss, parse_block_manifest now recomputes model_json_schema() for each step that uses a given dynamic block type, plus once in connections discovery. Before, repeated calls within the same compilation hit the cache. The extra cost is small next to the compile itself and doesn't apply on compilation-cache hits.

Commands that informed the review: gh pr view 3197, gh pr diff 3197, gh api (issue comments, review comments, reviews, commits), plus reading and grepping under review-source/workflows/roboflow_workflows/execution_engine/{introspection,v1/compiler,v1/dynamic_blocks} and review-source/workflows/tests/unit_tests/execution_engine/introspection/conftest.py.

Reviewed at HEAD: f64500d

@github-actions

Copy link
Copy Markdown
Contributor

Release coordination

  • Contributor: nothing more needed. The changelog entry is already under ## Unreleased → ### Execution engine in workflows/CHANGELOG.md.
  • Maintainers: at release time this change needs a version bump for roboflow-workflows and Execution Engine, because the engine's compile path behaviour changed (dynamic manifest caching). This notice doesn't block the PR.

Reviewed at HEAD: f64500d

@github-actions

Copy link
Copy Markdown
Contributor

😎 PR passes the vibe-check and trust-me-bro verification.

@dkosowski87
dkosowski87 merged commit c4c768c into main Oct 15, 2026
94 checks passed
@dkosowski87
dkosowski87 deleted the codex/avoid-global-dynamic-manifest-cache branch October 15, 2026 14:52
@roboflow roboflow deleted a comment from github-actions Bot Oct 15, 2026
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.

2 participants