Skip to content

[MiniMax-M3][LMCache] Drive ATOM's byte-level KV offload from the vLLM plugin - #2146

Open
XiaobingSuper wants to merge 14 commits into
mainfrom
perzhang/m3-vllm-lmcache-offload
Open

[MiniMax-M3][LMCache] Drive ATOM's byte-level KV offload from the vLLM plugin#2146
XiaobingSuper wants to merge 14 commits into
mainfrom
perzhang/m3-vllm-lmcache-offload

Conversation

@XiaobingSuper

@XiaobingSuper XiaobingSuper commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

What this does

Lets vLLM drive ATOM's existing byte-level LMCache offload, so the vllm-atom plugin path gets the KV offload tier the native engine already has. MiniMax-M3 is the model that forced it; nothing here is M3-specific except the one attention hook.

Validated end to end on 4×MI355X (TP=4, amd/MiniMax-M3-MXFP4, fp8 KV), with cudagraph and HBM prefix caching both enabled:

pass supplied by gsm8k 20-shot, 200q
cold all recomputed 0.92
replay 621,312 / 656,081 tokens (94.7%) via external_kv_transfer 0.935

No LMCache upgrade needed — validated on the image's stock LMCache 0.4.5, where the replay pass scores 0.945 against 0.945 cold with 621,312 tokens served externally, and the round trip is bit-exact. (0.4.5 is what rejected LMCacheConnectorV1 on M3 with unsupported kv_caches format with list depth 1 and tensor dimension 4; that is the path this branch does not use.) It also runs on 0.5.5rc4.

Plus a targeted correctness check (markers planted at 10/50/90% depth, HBM flushed with filler prompts in between): 11,008 tokens restored entirely from LMCache, all three markers reproduced.

Why not point vLLM at LMCache's own connector

LMCache's GPU connectors accept only the clean NHD/HND family and pick one format per model. M3 registers three physical layouts at once (dense K/V interleaved, sparse K/V in separate regions, plus a DSA index cache), and on ROCm the paths that could express that are unavailable anyway: the per-layer-format connector (V3) is off by default upstream and hangs on M3, and the multi-process path imports cupy, which LMCache's platform/rocm does not provide.

ATOM already solved this for its native engine by never asking LMCache to understand the layout: DenseKVByteCodec gathers whole paged blocks into a chunk-major uint8 blob and LMCache stores opaque bytes. That codec is reused verbatim here — no change to LMCache, no change to vLLM. This branch is the adapter that lets vLLM drive it.

Shape of the change

  • kv_cache_layout.py — vLLM hands a connector a flat {layer_name: tensor} and says nothing about what is inside. M3's sparse layers are not contiguous as a whole (stride(1) jumps the entire K region), so the tensor cannot be one segment, but t[:, 0] and t[:, 1] each are — exactly the k_cache/v_cache pair the codec expects. Dense layers stay whole; the DSA index caches fold into their owning layer.
  • connector.pyKVConnectorBase_V1 wrapper. Layer-granular hooks are inert on purpose: ATOM moves a whole request's blocks per transfer, and blocking a forward on a save would put the offload tier on the critical path it exists to keep clear.
  • seq_view.py — presents a vLLM Request as the seq ATOM's scheduler expects, as one stable instance per request (ATOM compares object identity to detect a recycled request id).
  • offload_config.py — derives ATOM's config from VllmConfig rather than duplicating block size and role.
  • dense/connector.py — two small additions on the ATOM side, see below.
  • 36 unit tests, built on M3's real strides.

Four defects found while validating (each is a commit with its own reasoning)

  1. fp8 KV scales did not travel. Every warm hit came back as fluent garbage while every metric reported success. M3's sparse cache stores fp8 mantissas whose scale is per token and per head, and that table lives on the attention layer — register_kv_caches hands over the caches only. A restored block was dequantised against whatever its previous occupant left behind; both sides are the right size and dtype, so nothing can fail loudly. The layer now reports its own scales (get_kv_transfer_scales, named after the native path's get_kv_transfer_tensors), with two hard errors so this cannot go quiet again: a layer holding multi-element scales it cannot report, and a reported scale whose leading axis is not the block axis.
  2. A promised load ATOM would not issue deadlocked the engine. A lookup hit is not a decision to load — ATOM drops hits that HBM already covers, that are not chunk aligned, or that fall below its transfer floor, and its native scheduler asks should_park_for_load_after_alloc before parking anything. vLLM has no second chance: async=True parks the request until the worker reports finished_recving, and a load that is never issued is never reported. Both routine drops fire constantly in the configurations we want (the floor defaults to 8192 tokens; with prefix caching on, vLLM's block-aligned frontier is regularly not chunk aligned). Symptom: EngineCore spinning in schedule(), every GPU idle, no log line at all.
  3. Worker completions never reached the scheduler half. update_connector_output was unimplemented, so nothing on the scheduler side ever finished — and every request whose save was still in flight kept its SeqView, which pins that request's prompt token ids. At M3's context lengths that is most of a megabyte retained per request.
  4. A watchdog for (2): a promise that goes undispatched now names itself in one ERROR line instead of hanging silently.

save_finished_by_request / load_finished_by_request are added to DenseOffloadScheduler for (3): vLLM's KVConnectorOutput carries request ids as plain strings, while save_finished/load_finished deliberately refuse a raw id once the lifecycle has an exact operation identity. Rather than weaken that guard for everyone, the plugin path gets a way to resolve the identity ATOM already parked.

Depends on

fix/m3-vllm-plugin-index-cache-fp8-dtype — M3 with --kv-cache-dtype fp8 needs the index cache to carry a real fp8 dtype, or the index-topk Triton kernel dots bf16 against uint8 on the first request. Independent of this branch; this one only depends on the result.

Known constraint, not introduced here

Use cudagraph_mode=FULL, not FULL_AND_PIECEWISE. M3 miscomputes under piecewise-captured graphs, and reusing a cached prefix is what exposes it: a resumed request prefills only a handful of tokens, which lands inside the capture sizes. Measured without LMCache and without this branch (plain vllm serve, default KV size, prefix caching on, same gsm8k twice):

cudagraph_mode cold (640 tokens local hit) replay (643,840 tokens local hit)
FULL_AND_PIECEWISE 0.935 0.19
FULL 0.935 0.950

PIECEWISE is worse still — it fails cold. NONE and FULL are both correct. This affects any M3 workload with prefix caching on, and stays invisible while the hit rate is near zero; it is being reported separately.

First piece of the vLLM-plugin offload path. vLLM hands a connector a flat
{layer_name: tensor} and says nothing about what is inside; ATOM's byte codec
wants the movable tensors named apart per layer, because it moves each as its
own contiguous segment. This is that translation and nothing else -- no
transfer policy, no LMCache.

MiniMax-M3 is the model that forced it. It registers three physical layouts at
once, and the sparse layers are not contiguous as a whole: stride(1) jumps the
entire K region (measured: shape (59454, 2, 128, 1, 128), stride (16384,
974094336, 128, 128, 1)). DenseKVByteCodec rejects non-contiguous segments, so
the tensor cannot be one segment -- but t[:, 0] and t[:, 1] each are, and that
is exactly the k_cache/v_cache pair the codec already expects. Dense layers
stay whole: their K and V interleave inside a block, so the block is one opaque
run. The DSA index caches, which vLLM registers as separate layers, fold into
their owning layer's index_cache field, which the codec already enumerates.

Layer order is numeric rather than dict order: segment order has to match
between save and restore, and registration order is not a contract.

The codec needs no change -- the tests build M3's real strides, run the mapping
through DenseKVByteCodec, and check bytes_per_block against the layout by hand.

(cherry picked from commit b723d764cd6f9051b6b893e93b70f6d22c85fb24)
Adds the adapter that lets vLLM's connector API run ATOM's existing offload
codec, so the plugin path gets the tier the native path already has.

Why not point vLLM at LMCache's own connector: its GPU connectors accept only
the clean NHD/HND family and pick ONE format per model. MiniMax-M3 registers
three layouts at once, and on ROCm the paths that could express that are
unavailable anyway -- the per-layer-format connector (V3) is off by default and
hangs on M3, and the multi-process path imports cupy, which LMCache's
platform/rocm does not provide. ATOM sidesteps all of it by never asking
LMCache to understand the layout: DenseKVByteCodec gathers whole paged blocks
into a chunk-major uint8 blob and LMCache stores opaque bytes.

Pieces:
- connector.py wraps ATOM's DenseOffloadConnector/Scheduler behind
  KVConnectorBase_V1. Layer-granular hooks are inert on purpose: ATOM moves a
  whole request's blocks per transfer, and blocking a forward on a save would
  put the offload tier on the critical path it exists to keep clear.
- seq_view.py presents a vLLM Request as the seq ATOM's scheduler expects.
  It must be one stable instance per request: ATOM stores the object and
  compares identity to detect a recycled request id, so a fresh wrapper per
  call would reset the load lifecycle every step.
- offload_config.py derives ATOM's three config fields from VllmConfig instead
  of duplicating block size and role.
- AtomOffloadMetadata wraps ATOM's metadata, which derives from ATOM's own base
  rather than vLLM's and so cannot be returned from build_connector_meta.

Registered by module path so importing the plugin does not drag LMCache into
every run; vLLM resolves it only when --kv-transfer-config names it.

Import and factory registration verified in the container against
vLLM 0.27.2.dev; 12 unit tests cover the mapping and the identity contract.

(cherry picked from commit 1ee388ebb81cf39d1b926b793f2b48989d39d2e0)
The first live run died with 'OffloadConfigShim has no attribute hf_config'.
The shim carried three fields because that is all dense/connector.py reads
directly; atom/kv_transfer/offload/config.py reads more -- hf_config (layer
count and the LMCache page namespace), kv_cache_dtype, PP geometry, and the
model name.

Grepped the whole offload package for config reads rather than patching one
attribute at a time. KV dtype needs translating: vLLM spells it fp8/auto/
bfloat16, ATOM indexes aiter.dtypes with fp8/bf16/fp16, and 'auto' means the
model dtype. Unknown spellings raise instead of defaulting -- the codec sizes
every byte segment from this, so a wrong guess mis-sizes transfers silently
rather than failing.

(cherry picked from commit fd9ede7acdcf5108934d2b11ed206190d9cd8b40)
Second live run: 'MiniMaxM3Config object has no attribute num_hidden_layers'.
Multimodal configs keep the transformer's own fields on a nested text config,
and M3's outer config genuinely has no layer count. vLLM exposes the resolved
inner one as model_config.hf_text_config while hf_config stays outer.

ATOM's offload code reads both spellings -- config.py notes the skew itself:
its namespace guard reads hf_config.model_type while is_qwen_next reads
hf_text_config.model_type. A view that resolves inner-first and falls back to
outer satisfies both without either side changing which attribute it asks for.

(cherry picked from commit 07d031d243328ba43da567f4fe3db6a9cbd76daa)
First live request died with 'get_finished() takes 1 positional argument but 2
were given': vLLM passes finished_req_ids, ATOM's takes none and returns its
own KVConnectorOutput.

The mapping is not mechanical. finished_saving must surface as vLLM's
finished_sending -- request_finished defers freeing while a save is in flight,
and vLLM releases those blocks only when the id shows up there. ATOM's worker
deliberately reports an EMPTY finished_sending because its own scheduler reads
that as a P/D producer handoff and would deallocate live offload blocks. Two
different contracts for one name, so the translation lives in the adapter
rather than changing either side.

Failed loads are reported as finished as well: the request is parked on that
load and the alternative to waking it is a hang; vLLM then recomputes what it
had counted as externally supplied. Completion ids come through as either a
bare request id or one tagged with a generation, so req_id is extracted.

(cherry picked from commit 639ae4b716405b97615c462087e4baf7e6c63346)
…uest is finished

Previous run died on vLLM's 'assert request.is_finished()' inside _free_blocks.
Reporting every finished_saving as finished_sending was too eager: vLLM treats
that set as 'this request is done AND its KV has shipped, free its blocks', and
asserts both req_id in self.requests and request.is_finished() before doing so.
ATOM's saves are fire-and-forget and routinely land mid-decode.

The two events arrive in either order -- a save can complete long before the
request stops generating, and a request can finish with a save still in flight
-- so each side is held until its counterpart shows up, and the pair is
released together.

Also adds a layout/dtype census at registration. The codec moves opaque bytes,
so a mislabelled tensor never fails there; it surfaces much later inside an
attention kernel ('Both operands must be same dtype') with nothing pointing
back at registration. Confirmed with it that vLLM hands us 3 dense uint8, 57
sparse uint8 and 57 index float8_e4m3fn -- the index caches do carry a real
fp8 dtype, which this path depends on:
`fix/m3-vllm-plugin-index-cache-fp8-dtype` is what makes them so.
Every warm hit came back as garbage. The metrics all said success --
external_prefix_cache_hits_total counted 10496 of 10539 prompt tokens
restored, LMCache reported the bytes stored and retrieved -- and the model
answered with '564,</$=$,</$=$,...'. Cold runs on the same prompts were
perfect.

M3's sparse cache stores fp8 mantissas whose scale is per token AND per head:
kv_scale is (2, num_blocks, num_kv_heads, block_size) fp32, one entry per
element of the paged cache. It lives on the attention layer, allocated lazily
on first forward. vLLM's register_kv_caches hands a connector the KV tensors
and nothing else, so the transfer moved mantissas only: a restored block was
dequantised against whatever scales its previous occupant had left in place.
Nothing can fail loudly there -- both sides are the right size and the right
dtype -- which is why 200 gsm8k questions scored 0.925 without ever exercising
a load.

The layer owns the layout, so the layer reports it: get_kv_transfer_scales()
on the sparse attention, named after the native path's get_kv_transfer_tensors,
which reports the same regions off runner.kv_scale. The connector passes the
layer modules (from vLLM's static forward context) alongside the tensors, and
the codec already enumerates k_scale/v_scale as segments -- no codec change.

It takes the registered tensor as an argument because registration runs before
the first forward, and _ensure_fp8_scales reallocates on a shape or device
change: sizing against the very tensor vLLM registered is also what keeps the
allocation stable, so a pointer handed to the tier stays live.

Two guards, because this failure mode is invisible at runtime:
- a layer holding a multi-element k_scale/v_scale with no way to report it is
  a hard error, not a silent omission (scalar per-tensor scales are constant
  and still ignored);
- a reported scale whose leading axis is not the block axis is rejected: the
  codec slices per-block as numel // num_blocks, and a head-major scale is the
  right dtype and nearly the right size, so nothing downstream would notice.

(cherry picked from commit 13b338565619e9c1151c70f1798352afcaeaa207)
vLLM runs a connector as two objects in two processes. ATOM's completion
objects only ever reach the worker half; the scheduler half hears about them
through update_connector_output, which this adapter did not implement. So on
the scheduler side nothing ever finished: _save_inflight and the load
lifecycle grew for the life of the process, has_pending_work() never went
quiet, and every request whose save was still in flight at finish kept its
SeqView -- which holds the request's prompt token ids. At M3's context lengths
that is most of a megabyte retained per request, for the life of the server.

vLLM's KVConnectorOutput carries request ids as plain strings, and both
save_finished and load_finished deliberately refuse a raw id once the
lifecycle has an exact operation identity, so that a delayed report cannot
complete a newer lifecycle. Rather than weaken that guard for everyone,
DenseOffloadScheduler grows save_finished_by_request / load_finished_by_request:
they resolve the identity ATOM already parked and delegate. The guard keeps its
meaning and the plugin path gets a way to speak.

(cherry picked from commit 0b32c55e35d295b13490048343a2397b882dc1a5)
Second gsm8k pass hung the server outright: 200 requests parked in
WAITING_FOR_REMOTE_KVS, EngineCore spinning in schedule() at 100% CPU with
every GPU at 0%, no log line for four minutes, client stuck at 0/200.

A lookup hit is not a decision to load. ATOM weighs that separately and drops
a hit that HBM already covers, one whose boundary is not chunk aligned, or one
below its transfer floor -- its native scheduler asks
should_park_for_load_after_alloc before parking anything. The adapter instead
promised async=True on the strength of the lookup alone, and vLLM has no
second chance: the only thing that releases a parked request is the worker
reporting it in finished_recving, and a load that is never issued is never
reported.

Both routine drops fire constantly in exactly the configurations we want:
- the floor defaults to 8192 tokens (OFFLOAD_MIN_LOAD_TOKENS), so every
  chat-sized prompt is dropped -- gsm8k's 2560 was a guaranteed hang;
- with HBM prefix caching on, vLLM's frontier is block aligned (128) while
  LMCache chunks are 256, so roughly every other hit is unaligned.

So the adapter asks before it promises, and reports no external tokens when
the answer is no. ATOM clears its own pending-load state on that path, so the
request just prefills normally.

Not caught earlier because the round-trip test uses 10K-token prompts, which
clear the 8192 floor. The tests here use a scheduler that reports a hit and
declines to load it -- the exact shape that deadlocked.

(cherry picked from commit a636c26b014fcced3c8465f95f18ea36f528f91f)
The park deadlock has no symptom of its own: EngineCore spins in schedule(),
every GPU sits at 0%, the stats logger goes quiet because nothing is running,
and the last log line is whatever happened before the stall. Tracing it back
to a connector promise took a py-spy dump of the engine and a read through
vLLM's waiting-queue code.

The promise is now gated on ATOM's own park decision, so this should stay
empty. If it does not, one ERROR line names the requests instead of leaving
a silent hang. Nothing here can rescue them -- only the worker's
finished_recving releases a parked request, and this is the scheduler half --
so it reports and moves on rather than pretending to recover.

Reported once per request, not once per step: a hung engine steps very fast.

(cherry picked from commit 2fef7b963ec666ee91f8f734dbf39edf41c7936a)
Copilot AI lite review requested due to automatic review settings September 6, 2026 08:31
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every eligible PR before approval:

  • ✅ Pre Checkin: Black, Ruff, catalog schema validation, non-GPU unit tests

Heavy model tests:

  • ✅ Run after the PR is approved and Pre Checkin passes
  • ✅ Run immediately when an approval review is submitted
  • ✅ Can be requested before approval with labels
Label Tests
ci:full Run all heavy PR model tests: native ATOM, vLLM, and SGLang
ci:atom Run native ATOM model accuracy tests
ci:vllm Run ATOM vLLM OOT model accuracy tests
ci:sglang Run ATOM SGLang model accuracy tests

Heavy jobs are skipped when the PR is not approved and no matching ci:* label is present.
Add labels via the sidebar or gh pr edit 2146 --add-label <label>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

There is at least one Black formatting issue in newly added code that is likely to fail the repo’s black --check CI gate.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adds a vLLM KV-connector adapter that lets vLLM drive ATOM’s existing byte-level LMCache offload path (including MiniMax-M3’s mixed KV layouts and fp8 per-token/per-head scale handling), aligning plugin-mode behavior with the native ATOM engine’s offload tier.

Changes:

  • Introduces a vLLM KVConnectorBase_V1 implementation that bridges vLLM KV-transfer hooks to ATOM’s dense LMCache offload scheduler/worker, including completion plumbing and “promised load” hang detection.
  • Adds vLLM→ATOM shims for request identity (SeqViewRegistry), config projection (OffloadConfigShim), and KV registration layout mapping (including splitting sparse K/V, folding index caches, and transporting fp8 scales).
  • Adds targeted unit tests covering identity semantics, config mapping, completion propagation/leak prevention, and M3-realistic stride/layout constraints.
File summaries
File Description
tests/plugin/test_vllm_offload_seq_view.py Validates SeqView identity + prompt-token keying + registry lifecycle.
tests/plugin/test_vllm_offload_config.py Tests vLLM→ATOM offload-config projection and dtype/block-size validation.
tests/plugin/test_vllm_offload_completions.py Ensures scheduler-side completion propagation, leak prevention, and promised-load watchdog behavior.
tests/plugin/test_vllm_kv_cache_layout.py Tests KV layout mapping for M3 (dense/sparse/index/scales) and codec compatibility.
atom/plugin/vllm/register.py Registers the connector name with vLLM’s connector factory (convenience path).
atom/plugin/vllm/kv_transfer/seq_view.py Provides a stable per-request seq wrapper matching ATOM offload scheduler expectations.
atom/plugin/vllm/kv_transfer/offload_config.py Projects VllmConfig into the minimal ATOM offload config surface.
atom/plugin/vllm/kv_transfer/kv_cache_layout.py Translates vLLM’s flat KV registration into ATOM KVCacheTensor segments (incl. fp8 scales/index).
atom/plugin/vllm/kv_transfer/connector.py Main adapter implementing vLLM v1 connector API over ATOM dense offload.
atom/plugin/vllm/kv_transfer/init.py Defines the kv_transfer package for the vLLM plugin adapter.
atom/plugin/vllm/attention/minimax_m3_attnetion.py Adds get_kv_transfer_scales hook so fp8 scales travel with KV bytes.
atom/kv_transfer/offload/dense/connector.py Adds request-id-based completion resolvers for the vLLM plugin path.
Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +79 to +80
def __repr__(self) -> str:
return f"_HFConfigView(inner={type(self._inner).__name__}, outer={type(self._outer).__name__})"
@zufayu
zufayu requested a review from yhl-amd September 7, 2026 01:16
The Black CI gate reformats three lines in the new files: a call that now
fits on one line, and two that no longer do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 7, 2026 02:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

zgplvyou and others added 2 commits September 6, 2026 22:25
vLLM auto-enables VLLM_USE_BREAKABLE_CUDAGRAPH for this architecture, so
plugin mode compiles nothing and splits nothing: one stream capture drives
the whole forward, and only ops carrying @eager_break_during_capture end a
segment. M3's sparse attention did not carry it, so 57 of the model's 60
attention layers were captured wholesale -- measured at capture time as
graphs=4, eager_breaks=3, the three breaks being the dense layers that go
through vLLM's own unified_attention. With the decorator: graphs=61,
eager_breaks=60.

Everything the sparse path reads per step was therefore frozen at whatever
the capture batch held: the prefill/decode token counts, block_table,
seq_lens, the topk indices. The damage stays invisible until a cache hit --
a cold prompt prefills past the largest captured size and runs eagerly, so
it answers correctly; reuse a prefix and the short remainder lands inside a
captured size, replays another batch's metadata, and answers fluently from
the wrong KV. Any M3 run with prefix caching on is exposed, and the lower
the hit rate the less it looks like a bug.

The decorator requires the op to write into a caller-owned buffer, so the
output allocation moves up into forward() and the op takes it as a mutated
argument; a tensor allocated inside would land at a new address on every
replay while the captured segments that consume it still read the address
recorded at capture.

Measured on MiniMax-M3-MXFP4, TP=4, cudagraph_mode=FULL_AND_PIECEWISE,
prefix caching on, no LMCache. A 12,628-token prompt with markers at 10%,
50% and 90% depth, cold then replayed against 12,544 locally cached tokens:
before, the replay missed the 50% and 90% markers and diverged; after, all
three come back. Full decode graphs are untouched -- they dispatch with
cudagraph_runtime_mode == FULL, which the decorator passes through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The defect this guards against is silent: drop the decorator and the sparse
layers go back inside the captured graph, cold prompts still answer
correctly, and only a prefix-cache hit exposes it. Confirming it costs a
two-run accuracy sweep on four GPUs.

The scan reads the source instead of importing it. The module imports aiter
at module scope, so an importing test would skip on every CI runner --
precisely where the guard needs to fire.

Both regression shapes were checked by hand: removing the decorator fails
the first test, emptying mutates_args fails the second.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 7, 2026 03:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

The block was already unsorted; adding an import to it put the existing
violation inside the PR's diff context, which is what reviewdog gates on.
vllm belongs in the third-party group with aiter and torch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 7, 2026 03:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

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