Skip to content

feat(examples): compare workflow and bespoke vision ensemble - #12924

Closed
furionw wants to merge 3 commits into
qiwa/vllm-workflow-componentsfrom
qiwa/workflow-vision-example
Closed

furionw wants to merge 3 commits into
qiwa/vllm-workflow-componentsfrom
qiwa/workflow-vision-example

Conversation

@furionw

@furionw furionw commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Based on #13293.

Why

Dynamo endpoints already let applications build this ensemble manually, so the workflow API should demonstrate its value against an equivalent baseline. Both variants share the inline components, stock vLLM worker, and the same GenerateEndpointInvoker for request validation, stream folding, and remote-call cancellation. This isolates the comparison to orchestration ownership: the workflow runtime owns graph scheduling and binding, while the bespoke handler owns coroutine and sibling-task lifecycle.

Effect

                  ┌─> inline classifier ────────────┐
request → encoder ┤                                 ├→ inline merge → chunk
                  └─> request adapter → stock vLLM ─┘

What Change

  • Add declarative and bespoke mixed-placement vision ensembles.
  • Share stages, launch topology, and Generate endpoint invocation.
  • Keep scheduling and sibling lifecycle as the comparison.
classDiagram
    WorkflowOrchestrator *-- StageDispatcher : schedules graph
    StageDispatcher ..> GenerateEndpointInvoker : invokes remote Generate
    BespokeEnsembleHandler ..> GenerateEndpointInvoker : invokes same adapter
    GenerateEndpointInvoker ..> DynamoClient : calls discovered endpoint
    BespokeEnsembleHandler --> EncoderStage : schedules inline

    note for WorkflowOrchestrator "Owns authored graph execution"
    note for BespokeEnsembleHandler "Owns equivalent task orchestration"
    note for GenerateEndpointInvoker "Shares validation, folding, and call cancellation"
Loading

Test Plan

  • Generate adapter and bespoke orchestration CPU behavior pass.
  • Targeted pre-commit hooks pass.

@github-actions github-actions Bot added feat documentation Improvements or additions to documentation labels Aug 10, 2026
@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

@furionw
furionw force-pushed the qiwa/workflow-embedded-vllm branch from 6960e40 to 84bb979 Compare August 10, 2026 07:16
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from ea1f87a to b6a62eb Compare August 10, 2026 07:16
@datadog-official

This comment has been minimized.

@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from b6a62eb to 6034f5f Compare August 10, 2026 07:20
@furionw
furionw force-pushed the qiwa/workflow-embedded-vllm branch 2 times, most recently from 6ee0251 to 809b5c2 Compare August 10, 2026 07:38
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from 6034f5f to 1f9a721 Compare August 10, 2026 07:38
@furionw
furionw force-pushed the qiwa/workflow-embedded-vllm branch from 809b5c2 to 2dca807 Compare August 11, 2026 06:11
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from 1f9a721 to 6df6a5f Compare August 11, 2026 06:12
@furionw
furionw force-pushed the qiwa/workflow-embedded-vllm branch from 2dca807 to fc5d9f9 Compare August 11, 2026 06:35
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from 6df6a5f to 6f734af Compare August 11, 2026 06:38
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from 6f734af to de41052 Compare August 11, 2026 06:40
@furionw
furionw force-pushed the qiwa/workflow-embedded-vllm branch from fc5d9f9 to 2302338 Compare August 11, 2026 06:42
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch 2 times, most recently from 171ff1d to 488d029 Compare August 11, 2026 16:12
@furionw
furionw force-pushed the qiwa/workflow-embedded-vllm branch from 2302338 to c388455 Compare August 11, 2026 16:47
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from 488d029 to f52ef49 Compare August 11, 2026 16:47
@furionw
furionw force-pushed the qiwa/workflow-embedded-vllm branch from c388455 to 83faf53 Compare August 11, 2026 16:51
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from f52ef49 to 1c7603b Compare August 11, 2026 16:52
@furionw
furionw force-pushed the qiwa/workflow-embedded-vllm branch from 83faf53 to ef94bc2 Compare August 12, 2026 01:38
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from 1c7603b to a4812f1 Compare August 12, 2026 01:38
@furionw
furionw force-pushed the qiwa/workflow-vllm-stage branch from 38615b3 to f6058a8 Compare August 12, 2026 21:58
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from c06bba2 to 76e03a8 Compare August 12, 2026 21:58
@furionw
furionw force-pushed the qiwa/workflow-vllm-stage branch from f6058a8 to 688dfc9 Compare August 12, 2026 22:31
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from 76e03a8 to d6d452e Compare August 12, 2026 22:31
@furionw
furionw force-pushed the qiwa/workflow-vllm-stage branch from 688dfc9 to d30320b Compare August 13, 2026 00:19
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from d6d452e to 11e7454 Compare August 13, 2026 00:19
"classifier": self._classifier,
"generator": decoder_stage,
}
plan = compile_workflow(define_workflow())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Does user have to author this worker.py for every single ensemble pipeline?
If so, this file seems to contain a lot of boilerplate code.
It might be a cleaner interface if a user only needs to author L87-L105 (the top portion of this start() function) to register the custom stages and workflow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

makes sense. will take this into consideration

backend,
resolved_model_config,
config.engine_args,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What does this adapter do?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

given an output of encoder, users may need to pass different kinds of Prompt into engine (vllm in this case).

e.g.

  • it may be a EmbedPrompt for LLM model
  • it may be a TokenPrompt for VLM model (because Qwen VL requires grid_thw alongside embedding)

I am seeing different cases where one of these prompts is needed.

merged_ensemble["classifier_scores"] = dict(scores)
merged_engine_data["ensemble"] = merged_ensemble
adapted["engine_data"] = merged_engine_data
return adapted

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If I'm not mistaken, this function is the post-processing step that stitches multiple outputs from different stages together, correct?

Even though this is pure Python logic without ML, would it be OK to model this step as a stage as well? That way, nothing lives outside of the workflow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

correct. it's totally doable to model it inside stage.

@furionw
furionw force-pushed the qiwa/workflow-vllm-stage branch from d30320b to d08f9fc Compare August 13, 2026 21:59
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch 2 times, most recently from 586253d to 4590bbf Compare August 13, 2026 23:14
@furionw
furionw force-pushed the qiwa/workflow-vllm-stage branch from fe3c7f8 to 59f743f Compare August 13, 2026 23:31
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from 4590bbf to 092393e Compare August 13, 2026 23:31
@furionw furionw changed the title feat(examples): author an aggregated vision workflow feat(examples): compare workflow and bespoke vision ensemble Aug 26, 2026
@furionw
furionw changed the base branch from qiwa/workflow-vllm-stage to qiwa/vllm-workflow-components August 26, 2026 04:21
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from 092393e to f187ca9 Compare August 26, 2026 04:21
@furionw
furionw changed the base branch from qiwa/vllm-workflow-components to qiwa/workflow-integrated-qwen-benchmark August 26, 2026 04:22
@furionw
furionw changed the base branch from qiwa/workflow-integrated-qwen-benchmark to qiwa/vllm-workflow-components August 26, 2026 04:23
@furionw
furionw force-pushed the qiwa/workflow-vision-example branch from f187ca9 to 940aa08 Compare August 26, 2026 04:25
@furionw
furionw force-pushed the qiwa/vllm-workflow-components branch from c01209e to fba8b57 Compare August 26, 2026 06:17

@caronzh03 caronzh03 left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

From user-interface perspective, the workflow approach seems cleaner, assuming this doesn't require a custom WorkflowEndpointHandler implementation like BespokeEnsembleHandler.

From functionality perspective, I cannot tell whether either approach can support remote stage calling in the future. For example, in bespoke approach, does the classifier_task have to be a local, in-process call? In workflow approach, does classifier stage have to be a local, in-process call?

I'm wondering if we can fuse the two approaches into one:

  • Let user-interface use the workflow approach - define graph topology without asyncio.create_task() boilerplate.
  • Let the Dynamo-provided WorkflowEndpointHandler be the default handler for workflow execution, and if users want to customize the execution logic, like adding prioritization or throttling, they can override WorkflowEndpointHandler, and at that point, the custom handler will look something like BespokeEnsembleHandler.

custom_template_path=str(CHAT_TEMPLATE),
ignore_weights=True,
)
await endpoint.serve_endpoint(WorkflowEndpointHandler(orchestrator).generate)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Where is WorkflowEndpointHandler defined?
Does user have to implement custom WorkflowEndpointHandler for each workflow?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Where is WorkflowEndpointHandler defined?

#13083

Does user have to implement custom WorkflowEndpointHandler for each workflow?

No, WorkflowEndpointHandler kicks off orchestrator which executes users' workflow.

@furionw

furionw commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor Author

From user-interface perspective, the workflow approach seems cleaner, assuming this doesn't require a custom WorkflowEndpointHandler implementation like BespokeEnsembleHandler.

the assumption is correct

Inline- and Remote-

From functionality perspective, I cannot tell whether either approach can support remote stage calling in the future. For example, in bespoke approach, does the classifier_task have to be a local, in-process call? In workflow approach, does classifier stage have to be a local, in-process call?

Existing Dynamo primitives already support it remote stage calling. both bespoke and workflow approach, can support remote stage calling.

Dynamo Primitive

tl;dr users' Python can

  • import Dynamo's runtime class, call runtime.endpoint("demo.backend.generate").serve_endpoint(my_function)
  • on client side, (await runtime.endpoint("demo.backend.generate").client()).generate(request)

Workflow (inline- and remote- switch)

the workflow abstract the above logic so that users only need to

  • define a stage (input, output and run function)
  • specify whether they want in-process or separate-process (e.g. remote): InlineBinding, RemoteBinding(endpoint_id)

This would allow users to switch between inline- and remote- easily. Does this abstraction make things more complicated than helpful?

// This PR
orchestrator = await WorkflowOrchestrator.bind(
    define_workflow(),
    bindings={
        "encoder": InlineBinding(encoder),
        "generator": GenerateEndpointBinding(GENERATOR_ENDPOINT),
        ...
    },
    runtime=runtime,
)

// hello-world example 
// https://github.com/ai-dynamo/dynamo/pull/13423/changes
orchestraor = WorkflowOrchestrator.bind(
    define_workflow(),
    bindings={
        stage_id: RemoteBinding(endpoint_id)
        for stage_id, endpoint_id in ENDPOINTS.items()
    },
    runtime=runtime,
)

@furionw

furionw commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

I'm wondering if we can fuse the two approaches into one:>

  • Let user-interface use the workflow approach - define graph topology without asyncio.create_task() boilerplate.
  • Let the Dynamo-provided WorkflowEndpointHandler be the default handler for workflow execution, and if users want to customize the execution logic, like adding prioritization or throttling, they can override WorkflowEndpointHandler, and at that point, the custom handler will look something like BespokeEnsembleHandler.

agreed.

  • there will be a point, where users need to customize beyond a static graph
    • we have users doing that today based on dynamo's endpoint primitives)
    • from workflow point of view, we can support Triton-BLS-like (business logic scripting) capabilities (loop, conditional, etc.) -- 4. Imperative authoring
  • dynamo has been supporting prioritization, requests admission (a form of throttling). If we proceed with workflow approach, Dynamo can also provide these optional utilities at stage level.

@caronzh03

Copy link
Copy Markdown

Does this abstraction make things more complicated than helpful?

The Workflow abstraction hides the execution details/complexity, giving users a clean way to define graph topology; it also decouples graph definition and instance binding nicely - reminding me of "dependency injection" design pattern in Java world, so I would definitely prefer using the WF approach.
In the bespoke approach, the graph topology definition and execution happens together in the Handler class. Perhaps if the graph is simple, there's not much difference in terms of maintenance effort, but if the graph becomes complex, and with remote calls, the code could be un-readable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend::vllm Relates to the vllm backend documentation Improvements or additions to documentation feat size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants