Skip to content

[Core] Profile persistent FlashInfer workspace before KV sizing - #102

Open
lesj0610 wants to merge 2 commits into
mainfrom
lesj/use-flashinfer-workspace-sizing-20260626
Open

lesj0610 wants to merge 2 commits into
mainfrom
lesj/use-flashinfer-workspace-sizing-20260626

Conversation

@lesj0610

@lesj0610 lesj0610 commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Purpose

A FlashInfer wrapper allocates its dedicated int workspace on its first plan(), and the shared float arena grows on demand, so on a fresh engine neither exists yet when determine_available_memory() runs. The closing measurement of memory_profiling therefore does not see them, and KV cache sizing is computed without them.

This PR adds a lifecycle that materializes that workspace inside the profiling window, keeps it alive with a lease until the closing measurement has read it, and locks the shared arena once the last warmup that could still size it has run.

AI assistance: Codex, Claude

What this changes

Three contracts.

Fail-closed support declaration. AttentionMetadataBuilder.get_persistent_workspace_memory_profiling_support() returns REQUIRED, NEUTRAL or UNSUPPORTED, and defaults to UNSUPPORTED. The new lifecycle runs only when every builder of the model declares REQUIRED or NEUTRAL and at least one declares REQUIRED. NEUTRAL means a builder has no reservation of its own but can be carried by the common lease; a builder that has not been taught the contract keeps the default and the model stays off the path. FlashInfer declares REQUIRED; GDN declares NEUTRAL so a hybrid model can still take it.

Lease lifetime. The shared arena is owned by the global workspace manager, but the int workspace is owned by its wrapper alone. Releasing the lease inside the profiling block frees it before the closing measurement total_consumed is derived from, so the lease has to outlive that block. The order is: initial profiling snapshot, persistent workspace reservation and profile run, closing profiling measurement, lease release, CUDA graph profiling, then rebind, warmup and lock as driven by the runner lifecycle.

Conservative reservation. This PR does not query per-plan workspace sizes. FlashInfer reserves its existing default arena and materializes the wrappers the enabled prefill and decode routes will use, while memory profiling is measuring allocator use, so the int workspace each one allocates falls inside the measured window. Direct trtllm-gen's module-global workspace is materialized the same way.

A model that does not opt in does not enter the new profiling-time reservation or post-warmup lock path; the existing capture-time behavior is unchanged.

Accounting for that persistent allocation can reduce the available KV capacity. The actual change has not been measured on this branch.

Test Plan

Static checks:

  • py_compile on the 12 changed Python files
  • ruff check
  • ruff format --check
  • git diff --check
  • pre-commit run mypy-3.10

Focused test:

python -m pytest \
  tests/v1/worker/test_cudagraph_memory_profiling.py \
  tests/v1/worker/test_gpu_worker.py -q

Test Result

Static checks passed. The focused run is 69 passed, 0 failed, 0 skipped.

It covers the support tri-state and its fail-closed default, REQUIRED + NEUTRAL composition on a mixed FlashInfer/GDN model, and the lease keeping a builder's int workspace alive through the closing measurement and released before CUDA graph profiling. It also covers the V1 and V2 ordering around that measurement, per-ubatch arena isolation, and arena size and pointer stability after the lock.

Notes

This PR deliberately leaves mm-prefix on the unsupported profiling path. It fixes the shared lifecycle for builders that opt in; mm-prefix will need a separate integration that materializes and retains its dedicated wrapper before it can safely opt in.

The results above come from CPU-only tests, against stubs and real WorkspaceManager instances. Not exercised on this branch:

  • an actual GPU profiling run, CUDA graph capture, or serving
  • the real KV cache memory and token count this reservation yields, and therefore the size of the capacity change described above
  • DCP, which answers UNSUPPORTED and is covered statically only
  • backend selection and other suites outside tests/v1/worker/

Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results.
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model. Not needed; this is an internal FlashInfer workspace accounting change.

@lesj0610 lesj0610 changed the title Use FlashInfer workspace sizing helper [Core] Use FlashInfer workspace sizing helper Jun 26, 2026
@lesj0610
lesj0610 marked this pull request as ready for review June 26, 2026 14:48
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@lesj0610
lesj0610 force-pushed the lesj/use-flashinfer-workspace-sizing-20260626 branch from e4ac21a to 7c9a1aa Compare June 26, 2026 15:41
@lesj0610
lesj0610 changed the base branch from lesj/nvfp4-qwen-gemma-integration-no-pr80-20260624 to main June 26, 2026 15:41
@lesj0610
lesj0610 force-pushed the lesj/use-flashinfer-workspace-sizing-20260626 branch from 561cfb5 to 733a786 Compare July 3, 2026 00:57
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 25a67f76-68c9-4dbf-a743-72df579e9264

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lesj0610
lesj0610 force-pushed the lesj/use-flashinfer-workspace-sizing-20260626 branch from 8dce53f to 5455d84 Compare September 1, 2026 13:39
@lesj0610
lesj0610 changed the base branch from main to lesj/flashinfer-mm-prefix September 12, 2026 07:06
@lesj0610
lesj0610 force-pushed the lesj/use-flashinfer-workspace-sizing-20260626 branch from 666abc3 to 137861d Compare September 14, 2026 04:21
@lesj0610
lesj0610 changed the base branch from lesj/flashinfer-mm-prefix to main September 14, 2026 04:21
…sizing

FlashInfer allocates its shared float arena and each wrapper's dedicated int
workspace lazily, on the first plan. Memory profiling runs before any of that,
so KV cache sizing is told the activation peak of a model whose attention
workspace does not exist yet, and the allocation lands afterwards -- against
memory KV sizing has already handed out, and in the CUDA graph case against an
arena execution has locked.

Give the builder a lifecycle the runners can drive. A builder declares
`PersistentWorkspaceProfilingSupport`, defaulting to `UNSUPPORTED` so an
unknown builder in a mixed-backend model cannot be assumed neutral. When every
builder of a model opts in, the runner reserves before the profiling
measurement, holds the builders and their wrappers alive through it with a
lease, rebinds them once the arenas are final, and locks the arena after the
last warmup that could still size it. A model that does not opt in keeps the
legacy path exactly, including no new lock: reserving nothing and then locking
would turn its later lazy allocation into a hard failure.

FlashInfer reserves conservatively: grow the shared arena to the default size
it would have reached anyway, then build the wrappers the active prefill and
decode routes will actually use, so their int workspaces are allocated inside
the measured window. Direct trtllm-gen's module-global workspace is
materialized the same way. Nothing here computes a size -- the point is only
that the allocations happen where profiling can see them, which can lower the
KV token budget by the amount the runtime used to take behind its back.

GDN declares NEUTRAL so a hybrid model can still take the path.

Signed-off-by: lesj0610 <lesj0610@users.noreply.github.com>
…sizing

FlashInfer allocates its shared float arena and each wrapper's dedicated int
workspace lazily, on the first plan. Memory profiling runs before any of that,
so KV cache sizing is told the activation peak of a model whose attention
workspace does not exist yet, and the allocation lands afterwards -- against
memory KV sizing has already handed out, and in the CUDA graph case against an
arena execution has locked.

Give the builder a lifecycle the runners can drive. A builder declares
`PersistentWorkspaceProfilingSupport`, defaulting to `UNSUPPORTED` so an
unknown builder in a mixed-backend model cannot be assumed neutral. When every
builder of a model opts in, the runner reserves before the profiling
measurement, holds the builders and their wrappers alive through it with a
lease, rebinds them once the arenas are final, and locks the arena after the
last warmup that could still size it. A model that does not opt in keeps the
legacy path exactly, including no new lock: reserving nothing and then locking
would turn its later lazy allocation into a hard failure.

The lease has to outlive the whole `memory_profiling` block, not just the
profile run inside it. Only the shared arena is held by the global manager; the
int workspace belongs to its wrapper alone, so releasing any earlier frees it
before the closing measurement `total_consumed` comes from. It is released
right after, before CUDA graph profiling rebuilds the minimal KV state.

FlashInfer reserves conservatively: grow the shared arena to the default size
it would have reached anyway, then build the wrappers the active prefill and
decode routes will actually use, so their int workspaces are allocated inside
the measured window. Direct trtllm-gen's module-global workspace is
materialized the same way. Nothing here computes a size -- the point is only
that the allocations happen where profiling can see them, which can lower the
KV token budget by the amount the runtime used to take behind its back.

That reservation owns the causal prefill wrapper, so a model whose prefill also
reaches a wrapper it does not own has to stay out: mm-prefix answers
`UNSUPPORTED` until the change that gives the reservation ownership of that
wrapper opts it back in. GDN declares `NEUTRAL` so a hybrid model can still
take the path.

Signed-off-by: lesj0610 <lesj0610@users.noreply.github.com>
@lesj0610
lesj0610 force-pushed the lesj/use-flashinfer-workspace-sizing-20260626 branch from 137861d to d90f87a Compare September 15, 2026 09:05
@lesj0610 lesj0610 changed the title [Core] Use FlashInfer workspace sizing helper [Core] Profile persistent FlashInfer workspace before KV sizing Sep 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.

1 participant