feat: Asynchronous pipeline creation#2604
Conversation
|
pkg.pr.new packages benchmark commit |
📊 Bundle Size Comparison
👀 Notable resultsStatic test results:No major changes. Dynamic test results:No major changes. 📋 All resultsClick to reveal the results table (355 entries).
If you wish to run a comparison for other, slower bundlers, run the 'Tree-shake test' from the GitHub Actions menu. |
Resolution Time Benchmark---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Random Branching (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.88, 1.82, 4.00, 6.47, 6.87, 8.97, 20.45, 22.86]
line [0.99, 1.90, 4.05, 6.65, 7.67, 11.88, 23.56, 25.95]
line [0.96, 1.91, 4.16, 7.04, 7.50, 11.16, 21.73, 24.85]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Linear Recursion (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.29, 0.55, 0.68, 0.82, 1.10, 1.10, 1.38, 1.45]
line [0.31, 0.52, 0.71, 0.86, 1.21, 1.26, 1.50, 1.67]
line [0.31, 0.53, 0.75, 0.83, 1.12, 1.28, 1.47, 1.59]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Full Tree (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.78, 2.13, 3.56, 6.05, 12.17, 25.04, 53.81, 107.41]
line [0.75, 1.91, 3.81, 5.91, 11.63, 25.35, 58.87, 116.88]
line [0.87, 2.05, 3.46, 6.46, 12.48, 26.60, 57.47, 116.50]
|
9fe9b72 to
de777f1
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces explicit pipeline initialization APIs for compute pipelines, enabling asynchronous compilation to reduce first-dispatch stalls, and updates docs/examples/tests to exercise the new behavior.
Changes:
- Refactors
ComputePipelineCoreand addsinitAsync()/initSync()to compute pipelines. - Updates multiple docs examples to eagerly initialize compute pipelines (primarily via
initAsync()). - Adds unit tests and updates docs/example snapshots to reflect the new initialization flow.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/typegpu/src/core/pipeline/computePipeline.ts | Adds initAsync/initSync, refactors memoization/initialization flow, and moves perf tracking into a helper. |
| packages/typegpu/tests/pipelineInit.test.ts | Adds tests covering sync/async initialization, caching, and unwrap behavior during async init. |
| packages/typegpu-testing-utility/src/extendedIt.ts | Extends the GPU device mock with createComputePipelineAsync for new tests. |
| apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts | Eagerly initializes all compute pipelines with initAsync() to avoid first-frame stalls. |
| apps/typegpu-docs/src/examples/simulation/gravity/index.ts | Eagerly initializes compute pipelines with initAsync(). |
| apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts | Eagerly initializes dynamically-created guarded compute pipelines with initAsync(). |
| apps/typegpu-docs/src/examples/image-processing/background-segmentation/index.ts | Eagerly initializes guarded compute pipelines via initSync() before use. |
| apps/typegpu-docs/src/examples/algorithms/mnist-inference/index.ts | Eagerly initializes compute pipelines via initAsync() (including optional subgroup pipeline). |
| apps/typegpu-docs/src/examples/algorithms/matrix-next/index.ts | Eagerly initializes both compute strategies via initAsync(). |
| apps/typegpu-docs/tests/individual-example-tests/stable-fluid.test.ts | Updates expected shader-module call count and snapshot output. |
| apps/typegpu-docs/tests/individual-example-tests/mnist-inference.test.ts | Updates shader snapshot output ordering/content. |
| apps/typegpu-docs/tests/individual-example-tests/matrix-next.test.ts | Updates expected shader-module call count and snapshot output. |
| apps/typegpu-docs/src/content/docs/apis/pipelines.mdx | Documents pipeline initialization and shows initSync/initAsync usage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| this.#initAsyncPromise = device | ||
| .createComputePipelineAsync({ | ||
| label: getName(this) ?? '<unnamed>', | ||
| layout: device.createPipelineLayout({ | ||
| label: `${getName(this) ?? '<unnamed>'} - Pipeline Layout`, | ||
| bindGroupLayouts: usedBindGroupLayouts.map((l) => this.root.unwrap(l)), | ||
| }), | ||
| compute: { module }, | ||
| }) | ||
| .then((pipeline) => { | ||
| this.#memo = { pipeline, usedBindGroupLayouts, catchall, logResources }; | ||
| this.#performanceTracker.measureCompile(device); | ||
| this.#initAsyncPromise = undefined; | ||
| }); |
| pipeline.initSync(); | ||
|
|
||
| await Promise.all(myPipelines.map((pipeline) => pipeline.initAsync())); |
There was a problem hiding this comment.
I'd make the snippets a bit easier to read, I think people are familiar with how to await multiple promises at once, so we can skip doing that ourselves here for the sake of readability
| pipeline.initSync(); | |
| await Promise.all(myPipelines.map((pipeline) => pipeline.initAsync())); | |
| pipeline.initSync(); | |
| // or | |
| await pipeline.initAsync(); |
| Pipeline initialization involves resolving the pipeline code, creating the shader module, and creating the underlying WebGPU pipeline. | ||
| This happens automatically the first time the pipeline is executed via `.draw`, `.dispatchWorkgroups`, or a similar method. | ||
| The `initSync` and `initAsync` methods let you perform this work ahead of time, avoiding a stall on first execution. |
There was a problem hiding this comment.
I'm missing the explanation of when one should use initSync as opposed to initAsync and vice versa. On the other hand, it's non-trivial to explain it briefly 🤔
Maybe:
| Pipeline initialization involves resolving the pipeline code, creating the shader module, and creating the underlying WebGPU pipeline. | |
| This happens automatically the first time the pipeline is executed via `.draw`, `.dispatchWorkgroups`, or a similar method. | |
| The `initSync` and `initAsync` methods let you perform this work ahead of time, avoiding a stall on first execution. | |
| Pipeline initialization involves resolving the pipeline code, creating the shader module, and creating the underlying WebGPU pipeline. | |
| This happens automatically the first time the pipeline is executed via `.draw`, `.dispatchWorkgroups`, or a similar method. | |
| The `initSync` function lets you jump-start that initialization on the WebGPU process early. To await until the initialization actually finishes and avoid a stall on first execution, you can use the `initAsync` function instead. |
| const prepareModelInputPipeline = root | ||
| .with(paramsAccess, paramsUniform) | ||
| .createGuardedComputePipeline(prepareModelInput); | ||
| prepareModelInputPipeline.pipeline.initSync(); |
There was a problem hiding this comment.
I'd prefer if we had the initSync and initAsync methods on guarded compute pipelines as well. This would make the helper functions that catch errors a lot simpler on both the type level and at runtime, since they wouldn't have to branch on whether something is a guarded pipeline or not, they would just call the initSync or initAsync function.
| }, | ||
| }); | ||
|
|
||
| await Promise.all([computeCollisionsPipeline.initAsync(), computeGravityPipeline.initAsync()]); |
There was a problem hiding this comment.
I'd prewarm these promises early:
| await Promise.all([computeCollisionsPipeline.initAsync(), computeGravityPipeline.initAsync()]); | |
| // Initializing all used pipelines | |
| const pipelinePromises = [ | |
| computeCollisionsPipeline.initAsync(), | |
| computeGravityPipeline.initAsync(), | |
| skyBoxPipeline.initAsync(), | |
| renderPipeline.initAsync(), | |
| ]; |
And await them right before requesting the first frame:
// Only requesting the first frame after all pipelines have been initialized
await Promise.all(pipelinePromises);
requestAnimationFrame(frame);On another note, why aren't we preinitializing the render pipelines?
Changes:
ComputePipelineCore,initAsyncandinitSync,initAsync,The PERF is set to true only in browser tests, which currently do not work.
Also, I think that the measurements are just incorrect.
Still, I opted to leave them as is in a separate class. I tested manually the results before and after on Stable Fluids (7 resolves), and they were similar, so I don't think any logic changed.
For 9 pipelines in StableFluids, the async initialization actually seems slower (0.015s for the await) than sync (0.008s).
Please, @reczkok @iwoplaza @cieplypolar let me know if this looks alright, then I'll update skills and render pipeline (probably 1:1 changes from compute).