Skip to content

Add shared thread-pool to HTTP API workflows runner - #1717

Merged
hansent merged 3 commits into
mainfrom
feature/shared-thread-pool
Nov 14, 2025
Merged

hansent merged 3 commits into
mainfrom
feature/shared-thread-pool

Conversation

@PawelPeczek-Roboflow

Copy link
Copy Markdown
Collaborator

Description

Adding shared thread-pool executor for workflows

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

How has this change been tested, please provide a testcase or example of how you tested the change?

  • e2e with throughput increase verified

Any specific deployment considerations

For example, documentation changes, usability, usage/costs, secrets, etc.

Docs

  • Docs updated? What were the changes:

Comment thread inference/core/env.py
os.getenv("HTTP_API_SHARED_WORKFLOWS_THREAD_POOL_ENABLED", "True")
)
HTTP_API_SHARED_WORKFLOWS_THREAD_POOL_WORKERS = int(
os.getenv("HTTP_API_SHARED_WORKFLOWS_THREAD_POOL_WORKERS", "16")

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.

seems reasonable. did we try differetn values? in past ive based default for this kind of thing on # of cpus available

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

no, purely gut-feeling-based default

@hansent
hansent merged commit c4077f1 into main Nov 14, 2025
1 check passed
@hansent
hansent deleted the feature/shared-thread-pool branch November 14, 2025 21:52
davidnichols-ops added a commit to davidnichols-ops/inference that referenced this pull request Sep 23, 2026
Build a single ThreadPoolExecutor per workflow run when the host passes no
executor instead of one per DAG wave: create it lazily on the first wave
that runs, dispatch each wave with a flat map (flat_dispatch) so scheduling
matches the historical executor-less path, and shut it down in finally
before stream-pipeline close so wave tasks drain in the same order the
per-wave with-blocks did. Host-provided pools keep batched dispatch and are
never built or shut by the engine, so the HTTP server and streaming
pipeline are unchanged.

Worker threads live for exactly one run. Framework context stays isolated
per task through context snapshots and the unconditional rebinds in
safe_execute_step; third-party threading.local set inside one step may be
seen by later steps of the same run (the reuse profile hosted pools have
had since roboflow#1717), and cannot cross runs because the threads are destroyed
at run end.

On a six-round counterbalanced benchmark, pool constructions fall from the
wave count to one and median run latency drops from 1.10 to 0.71 ms on a
10-step chain (-35%) and from 5.08 to 3.01 ms on a 50-step chain (-41%),
with single-wave, host-provided, and compute-heavy cells unchanged within
noise. The 16 regression tests fail on unpatched base code with 10
failures (pool counts, dispatch shape, shutdown ordering, early-
termination teardown) and pass here; suites report 6599 passed, 113
skipped (workflows package) and 1238 passed, 30 skipped (root server
workflows).
davidnichols-ops added a commit to davidnichols-ops/inference that referenced this pull request Sep 23, 2026
Build a single ThreadPoolExecutor per workflow run when the host passes no
executor instead of one per DAG wave: create it lazily on the first wave
that runs and dispatch each wave flat (flat_dispatch - submit the whole
wave, collect results) so scheduling matches the historical executor-less
path. A step error drains in-flight siblings before leaving
run_steps_in_parallel, exactly where the per-wave with-blocks used to
join, and the pool shuts down in a nested finally before stream-pipeline
close so pipeline cleanup still runs if shutdown itself raises.
Host-provided pools keep batched dispatch and are never built or shut by
the engine, so the HTTP server and streaming pipeline are unchanged.

Worker threads live for exactly one run. Framework context stays isolated
per task through context snapshots and the unconditional rebinds in
safe_execute_step; third-party threading.local set inside one step may be
seen by later steps of the same run (the reuse profile hosted pools have
had since roboflow#1717), and cannot cross runs because the threads are destroyed
at run end.

On a six-round counterbalanced benchmark, pool constructions fall from the
wave count to one and median run latency drops from 1.10 to 0.71 ms on a
10-step chain (-35%) and from 5.08 to 3.01 ms on a 50-step chain (-41%),
with single-wave, host-provided, and compute-heavy cells unchanged within
noise. The 17 regression tests fail on unpatched base code with 11
failures (pool counts, dispatch shape, shutdown ordering,
early-termination teardown) and pass here; suites report 6607 passed, 113
skipped (workflows package) and 1238 passed, 30 skipped (root server
workflows).
davidnichols-ops added a commit to davidnichols-ops/inference that referenced this pull request Sep 24, 2026
Build a single ThreadPoolExecutor per workflow run when the host passes no
executor instead of one per DAG wave: create it lazily on the first wave
that runs and dispatch each wave flat (flat_dispatch - submit the whole
wave, collect results) so scheduling matches the historical executor-less
path. A step error drains in-flight siblings before leaving
run_steps_in_parallel, exactly where the per-wave with-blocks used to
join, and the pool shuts down in a nested finally before stream-pipeline
close so pipeline cleanup still runs if shutdown itself raises.
Host-provided pools keep batched dispatch and are never built or shut by
the engine, so the HTTP server and streaming pipeline are unchanged.

Worker threads live for exactly one run. Framework context stays isolated
per task through context snapshots and the unconditional rebinds in
safe_execute_step; third-party threading.local set inside one step may be
seen by later steps of the same run (the reuse profile hosted pools have
had since roboflow#1717), and cannot cross runs because the threads are destroyed
at run end.

On a six-round counterbalanced benchmark, pool constructions fall from the
wave count to one and median run latency drops from 1.10 to 0.71 ms on a
10-step chain (-35%) and from 5.08 to 3.01 ms on a 50-step chain (-41%),
with single-wave, host-provided, and compute-heavy cells unchanged within
noise. The 17 regression tests fail on unpatched base code with 11
failures (pool counts, dispatch shape, shutdown ordering,
early-termination teardown) and pass here; suites report 6607 passed, 113
skipped (workflows package) and 1238 passed, 30 skipped (root server
workflows).
PawelPeczek-Roboflow added a commit that referenced this pull request Sep 25, 2026
…uns (#3052)

Build a single ThreadPoolExecutor per workflow run when the host passes no
executor instead of one per DAG wave: create it lazily on the first wave
that runs and dispatch each wave flat (flat_dispatch - submit the whole
wave, collect results) so scheduling matches the historical executor-less
path. A step error drains in-flight siblings before leaving
run_steps_in_parallel, exactly where the per-wave with-blocks used to
join, and the pool shuts down in a nested finally before stream-pipeline
close so pipeline cleanup still runs if shutdown itself raises.
Host-provided pools keep batched dispatch and are never built or shut by
the engine, so the HTTP server and streaming pipeline are unchanged.

Worker threads live for exactly one run. Framework context stays isolated
per task through context snapshots and the unconditional rebinds in
safe_execute_step; third-party threading.local set inside one step may be
seen by later steps of the same run (the reuse profile hosted pools have
had since #1717), and cannot cross runs because the threads are destroyed
at run end.

On a six-round counterbalanced benchmark, pool constructions fall from the
wave count to one and median run latency drops from 1.10 to 0.71 ms on a
10-step chain (-35%) and from 5.08 to 3.01 ms on a 50-step chain (-41%),
with single-wave, host-provided, and compute-heavy cells unchanged within
noise. The 17 regression tests fail on unpatched base code with 11
failures (pool counts, dispatch shape, shutdown ordering,
early-termination teardown) and pass here; suites report 6607 passed, 113
skipped (workflows package) and 1238 passed, 30 skipped (root server
workflows).

Co-authored-by: Paweł Pęczek <[email protected]>
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