Repository navigation
Conversation
…dent byte checks
Two gaps in the UCM model compatibility checker:
1. The Ascend KV layout could only be checked on an NPU, while the CPU
launcher reproduces the official (CUDA) layout, which is a different
design. Add `ascend_sim.py`: vLLM-Ascend's own model construction,
KVCacheSpec, grouping, KVCacheConfig and
`NPUModelRunner.initialize_kv_cache()` run with the KV tensors in host
memory, so the Ascend layout can be verified with the driver libraries
present but no `/dev/davinci*`. Only execution-ordering APIs, weights and
compute kernels are substituted; every substitution and its layout-neutral
reason is listed in the README. The SoC family is reported from the
installed wheel's build info rather than simulated, so `--ascend-soc` only
asserts a match.
2. The connector-v2 verification derived every address from the connector's
own TransferBuilder, so a symmetric addressing error cancelled out and a
layer the connector never transferred was never inspected. Add checks that
use only vLLM's tensors, specs and block tables:
- native oracle: target block i equals source block i byte-for-byte, over
persistent full-attention groups and whole-block sliding-window groups;
- stray writes: the load changes no KV byte outside its target ranges;
- hit length: loaded FA chunks are the reachable prefix of the dumped ones
and agree with the scheduler's external hit.
Fault injection confirms these are not vacuous: dropping one layer from
every transfer keeps the connector-derived comparison green and fails the
native oracle, for a full-attention and for a sliding-window layer.
Also: the legacy path's source fill becomes position-dependent bytes written
through a uint8 alias (a constant per block hid intra-block offset errors, and
distinct values could round together in fp8/bf16); comparisons are byte-wise,
so NaN payloads with equal bits compare equal; the FAWA path skips transient
groups instead of refusing to classify them; the local prefix-hash eviction
before the target lookup now also runs on the legacy path; and the fixed
10 s wait after dump is configurable and defaults to 0 for the synchronous
v2 proxy.
Verified on vLLM 0.30.0 / vLLM-Ascend 0.30.0rc1 and official vLLM 0.30.0+cpu,
UCM built with PLATFORM unset: DeepSeek-V4.1-Flash passes bulk, layerwise and
legacy-connector runs without a device, and Qwen-Flash-Next passes on the CPU
launcher. The segment counts match those recorded for the same models on real
hardware. README limitations are re-checked against 0.30: DeepSeek-V4.1 on the
CPU launcher now stops at the ScaledMM linear kernel, not the removed grouping
assertion, and CPU external load does work on 0.30 with connector v2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…stent group The previous checks could only assert bytes for groups whose storage policy the checker can predict: full attention (the whole hit prefix) and a sliding window that is exactly one block (its boundary block). Everything else was reported as "not natively assertable" and skipped -- which left DeepSeek-V4.1's window groups only partly covered and Qwen-Flash-Next's four MambaSpec state groups not covered at all. Predicting where a state snapshot lands means reimplementing the runner's load-slot rule, and the only source for that rule is the connector under test. Replace the question. Instead of asking which blocks must be restored, require that no loaded byte holds a value UCM never stored: after the load, every byte of a poisoned target block is either the source byte at the same position (restored) or still 0xff (not restored). Patterns only use [0,250], so the two cases cannot be confused, and a third value can only come from data placed at the wrong offset, block, layer or group. That assertion contains no storage policy, so it covers sliding-window, state-snapshot and future group kinds without the checker having to model them. Restored/total bytes per group are logged as a report rather than asserted, because a group legitimately restores only part of its blocks. The strict equality check stays for the groups it can cover; the two are complementary (narrow and strong vs wide and weaker). Also drop the one place that read connector internals: the hit-length check used the lookup's private recompute window, and now bounds the restorable prefix from the request length instead. Verified without an accelerator (vLLM-Ascend 0.30.0rc1, driver libraries mounted read-only, no /dev/davinci*): DeepSeek-V4.1-Flash passes with the black-box check covering all 11 persistent groups (176 blocks, every group restoring 100% of its poisoned bytes) next to 124 strict blocks. On the CPU launcher Qwen-Flash-Next passes with 5 groups / 194 blocks covered, including the four MambaSpec groups the strict oracle skips. Peak RSS of a full DeepSeek-V4.1-Flash run is 2.8 GB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ljccsu
requested review from
SuperMarioYL,
dante159753,
harrisonyhq and
student-jhz
as code owners
October 29, 2026 02:16
`ascend_sim.py` checks the Ascend layout without an NPU, but the official (CUDA) layout had no device-less path: `cpu.py` reproduces it on a CPU build through hand-declared backend constraints, which is a different thing from running vLLM's real CUDA attention backends. Add `cuda_sim.py` so both platforms can be checked the same way, and move what the two launchers share into `sim_runtime.py` (host streams and events, pinned memory, UCM's host device) so they cannot drift. `cuda_sim.py` needs a CUDA build of vLLM: its attention backends import their kernel entry points at module level behind `if current_platform.is_cuda()`, so the real backend classes -- the ones that compute the KV shapes and layouts -- are only importable there. The launcher checks for `vllm.vllm_flash_attn` and explains that, rather than failing deep inside vLLM. Two substitutions are specific to CUDA. vLLM resolves its platform by asking NVML for a device count, so with no GPU the CPU platform would win: the launcher replaces that probe with a declaration, which answers "is this hardware present" rather than "what does the layout look like", and refuses to run if anything resolved the platform first. And the compute capability cannot be read without a device while vLLM branches on it (FlashAttention version, fp8 support, MLA backend choice), so it is declared with `--cuda-capability` and never guessed -- the same rule `ascend_sim.py` applies to the Ascend SoC family, which it asserts against the installed wheel's build info. A PASS is therefore a statement about the layout vLLM produces for that capability, and the value is logged. Kernel-availability probes are answered from the same declaration rather than skipped, so backend selection stays native and the chosen backend appears in the layout log. KV-cache specs, grouping, shapes, strides, dtypes and block IDs are not touched, as in the other launchers. Not yet run: the available host is aarch64 with no CUDA vLLM wheel installed, so these substitutions are the set vLLM 0.30's source predicts is required, not a verified one -- the README says so. Unit tests cover the capability parsing (dotted and nvcc forms, refusal to invent one), the CPU-build refusal and the late-platform-resolution refusal; the toolkit tests cover the new `--platform cuda-sim` routing and that it asks for no visible device. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review fixes on the device-less CUDA launcher: * declare the platform by mutating `builtin_platform_plugins` in place. `vllm.platforms` defines its own `__setattr__`, so rebinding the name could have been intercepted; * decide "did vLLM already resolve its platform" from `_init_trace`, the marker vLLM itself writes when it resolves, with the cached platform as a fallback. Reading `current_platform` instead would have done the resolving; * report `cuda-sim` from `doctor()` beside `ascend-sim`; * drop the `rebind` import `ascend_sim.py` no longer uses after the shared helpers moved to `sim_runtime.py`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review of 199c4de found four places where a check could pass on code that is wrong. All four are confirmed in the code, and the first three made the verdict itself unreliable rather than merely narrow. 1. The stray-write check took its allowed set from the connector's own transfer addresses, so a connector writing an unrelated target block had that wrong address authorized before the check ran. The allowed set now comes only from vLLM's block tables and views, and the connector's ranges are verified against it (`check_transfers_inside_native_blocks`); a range leaving it fails with its address. `StorageSnapshot.contains()` makes a reported address checkable instead of trustworthy. 2. The hit-length check derived its expectation from the connector's own dump plan (`expected = min(len(dumped), reachable)`), so a truncated dump shrank its own expectation and a save and a load that were wrong together agreed. The dumped count is now required to equal what the source request's scheduled range and the chunk granularity imply, and the loaded count is bounded from the target request's length -- two independent assertions. 3. The black-box check accepts a byte that stayed `0xff`, which is what lets it cover groups whose storage policy the checker cannot derive, but it was reported as if it proved restoration. It is a placement check: a group whose bytes all stayed poison passes it. Each group now gets a verdict saying whether an independent required-restore set backed it, and the run reports the groups without one as PARTIALLY VERIFIED. 4. `cuda_sim.py` forced `flash_attn_supports_fp8` and `is_fa_version_supported` to True, which changes vLLM's backend support rules -- so the claim that backend selection stayed native was wrong. Those substitutions are gone: only the hardware identity is declared, and vLLM's own capability and availability rules keep running against it. Also: a pattern was one arithmetic sequence modulo 251 and so repeated every 251 bytes, hiding a swap of equal-length pieces that far apart. Patterns now sum one sequence per scale, which moves the repeat to 251**3 (about 15.8 MB, far beyond one KV block) and makes every smaller shift visible. The README no longer claims more than the checks deliver. `tests/test_model_check_blind_spots.py` turns the review's reproductions into 10 regression tests: shifts that used to be invisible, an address outside the allocated block, and a group with no required-restore set reading as partial. 34 tests pass locally; isort, black and pyflakes are clean. No model, device or store run was performed for this commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…with -m One command per model, in the fixed case order the README calls the standard flow: layout-only, then bulk, then layerwise. It resolves the repo root from its own path, puts UCM's built `.so` directories on LD_LIBRARY_PATH, prints a preflight showing which of torch / ucm / wrapt / vllm / vllm_ascend are importable, writes one log and one store per case, and prints a summary whose exit code is 0 only when every selected case passed. A case counts as passed only when the checker itself printed its PASS verdict. The exit code alone is not enough, and this turned up a real false pass while testing the script: `python -m ucm_toolkit.cli` silently did nothing and exited 0, because cli.py had no `__main__` guard, so a run that never reached the checker was reported as a pass. The guard is added, and the script now requires the verdict line as evidence. The summary repeats what a PASS does and does not cover, and points at the PARTIALLY VERIFIED lines for the groups that have no independent required-restore set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ust the env
Measured on the Huawei host: dsv41-flash passes all three cases on
`vllm-ascend:v0.30.0rc1` (triton-ascend 3.2.2) and fails all three on
`v0.30.0rc2` (triton-ascend 3.6.0+dev) with
Ascend environment variable 'TRITON_ASCEND_ARCH' is deprecated ...
host environment overrides are ignored.
cann_get_soc_name failed.
SystemError: <built-in function get_arch> returned NULL
The three cases did not fail for three reasons: they all stop at the same
import-time query, inside vLLM-Ascend's patch chain pulling in vLLM's
flash-linear-attention ops, whose module body asks triton for the current
target. That is before any KV spec, grouping or dump/load work, so this was a
gap in the launcher's device-less adaptation, not a statement about whether
rc2 supports the model.
Setting `TRITON_ASCEND_ARCH` is no longer enough, so declare the arch where
the query actually happens: the driver's own `get_arch`, whichever class owns
it, which also covers instances already created. `get_aicore_num` on the same
object reaches the device too and is a kernel launch parameter, so it gets the
same placeholder as the compressor's core count. The environment variable is
still set, because older triton honours it. If a future triton moves the
lookup, the launcher now fails with the triton version and the expected error
text named instead of letting the device query through.
`tests/test_model_check_ascend_sim.py` covers both shapes with fake triton
modules: the rc2 path where only the lookup works, the rc1 path where the
environment variable is also set, a renamed lookup failing loudly, and a
missing Ascend backend staying non-fatal.
README: record that the toolkit needs `--no-build-isolation` on these images
(they ship setuptools but an isolated build tries to download it), that
`wrapt` must be installed, and the triton arch difference between the two
images.
Not verified: this fix has not been run on rc2. It is written from the
reported traceback and the 3.2.2 driver source; rc2 needs a rerun of all
three cases before anyone calls its device-less path adapted. Whether UCM
works with rc2 on a real card is a separate question that nothing here tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rpreters
Measured on rc2 with the previous commit: the arch declaration worked in the
main process -- the log shows `NPUUtils.get_arch` and `NPUUtils.get_aicore_num`
replaced, and the import chain that used to fail got past -- but all three
cases still failed. vLLM inspects a model architecture in a **fresh
interpreter** (`subprocess.run([sys.executable, "-m",
vllm.model_executor.models.registry])`), whose `_run()` calls
`load_general_plugins()` first. A monkey patch does not travel there, the
environment override is ignored by this triton, and the child repeated the
device query:
registry.py:1570 in _run -> load_general_plugins() -> ...
triton.runtime.driver.active.get_current_target().backend
SystemError: <built-in function get_arch> returned NULL
ValidationError: Model architectures ['DeepseekV41ForCausalLM']
failed to be inspected.
Move the declarations into `sim_bootstrap.py`, a module that imports without
torch, vLLM or UCM, and reach every interpreter through a generated
`sitecustomize.py`: Python's `site` imports that name at startup, before the
`-m` module runs. The parent calls the same functions directly, so there is one
implementation instead of two that can drift. The generated file chains to any
`sitecustomize` it shadows, does nothing unless a simulated platform is
declared for the run, and warns on stderr instead of raising -- replacing a
clear error with an unrelated crash in an unrelated interpreter would be worse.
`tests/test_model_check_sim_bootstrap.py` really spawns interpreters with a
fake triton whose lookups fail the way the real ones do, so the child path is
covered rather than assumed: without the bootstrap a child reproduces
`get_arch returned NULL`, with it the child reports the declared arch, an
explicit arch reaches it too, a shadowed `sitecustomize` still runs, an
undeclarable arch warns without killing the child, and the generated file is
compiled so a name error in it cannot fail silently at startup. That last test
earned its place immediately: an earlier blind `sed` in this session deleted
`import sys` from the embedded template, and the only symptom was the
bootstrap quietly doing nothing.
55 tests pass locally; isort, black and pyflakes are clean.
Not verified: rc2 has not been rerun with this. Whether UCM works with rc2 on a
real card remains a separate, untested question -- nothing here speaks to it.
The rc2 failures so far are all the absence of a device, not a sign that the
image or the model is unsupported.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The generated sitecustomize chained to the file it shadowed by popping itself
from sys.modules and re-importing the name. When the image has no sitecustomize
of its own -- the normal case -- that leaves site's own import with no module to
return, so every interpreter prints
Error in sitecustomize; set PYTHONVERBOSE for traceback:
KeyError: 'sitecustomize'
The declaration itself still works, so no existing test noticed: a real child
exits 0 with the declared arch, the only symptom being the stderr line that every
child of the run carries, and that vLLM pastes into its "Error raised in
subprocess" report whenever a later failure happens.
Load the shadowed file by path and execute it instead, keeping the bootstrap
directory out of sys.path afterwards as before. The new test asserts the child's
stderr is empty when nothing is shadowed; "a shadowed sitecustomize still runs"
keeps covering the other half.
Not verified: rc2 has not been rerun with this or the previous commit.
…ment rc2 reported 54 passed / 1 failed where a clean checkout passes 55. The failing test is the one that asserts the bootstrap warns when there is no arch to declare: it removed `SOC_VERSION` from the dict it passes, but `_run_child` started from `os.environ.copy()`, and the vLLM-Ascend images export `SOC_VERSION` themselves, so the child could declare an arch after all and the expected warning never appeared. The test was asserting a precondition it did not actually establish. Build the child environment by dropping every variable that changes what the bootstrap declares, then applying only what the test states. A test now reads the same way on a bare host and inside an image that exports these itself. The one test that built its own environment inline does the same. Verified both ways locally: with `SOC_VERSION` and `TRITON_ASCEND_ARCH` exported, the suite failed exactly as reported before this change and passes after it -- 56 tests, clean under `isort`, `black` and `pyflakes`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three findings from re-reading the flow after the rc2 run. The transfer-range check ran after the poisoning that uses the very addresses it validates. A connector reporting an address outside the request's blocks would already have had that memory poisoned -- possibly another group's source data, which then fails the comparison and points at the wrong place. The check moves ahead of the poisoning (`verify_transfer_ranges` builds its own allowed set from vLLM's block tables), and the stray-write snapshot afterwards only establishes the baseline. The dumped-chunk expectation came from the source request's length. FA stores every complete chunk of the *computed* range, which equals the prompt only while the prompt fits one scheduling step; with chunked prefill a long prompt is split and the expectation would ask for chunks the connector was never told to store. It now reads the scheduler's own per-request token count, and falls back to the request length where that is unavailable. The generated bootstrap directory had a fixed name under the temp directory, so two runs on one host shared it and one run's declarations could reach the other's children. The name now carries the pid, with a test for it. 57 tests pass, including with SOC_VERSION exported; isort, black and pyflakes clean. None of this has been rerun on the server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upstream refactored connector v2 while this branch sat on 5fafa6a: the v2 files were reorganised, `UCMConnector` became a facade over `UCMConnectorWorker`, `tokens_per_chunk` became `chunk_size` and `attention_type` became `restore_policy`. Upstream had already followed those moves in `_v2_batch`; the functions added on this branch had not. A static check of the symbols this checker reads showed ten of them absent at the new upstream tip, so merging without adapting would have broken the v2 path -- and `check_v2_hit` would have *silently skipped* itself, which is worse than a crash: the run would skip the hit check and still report PASS. So the worker lookup is now explicit and fails loudly. `v2_worker_internals()` finds the object carrying `spec` / `layout` / `transfer_builder`, trying the object itself, `connector_worker` and `connector` (and one level deeper), and raises with what it inspected and which list to extend when it cannot. The two renamed fields go through `spec_chunk_tokens()` and `plan_restore_policy()`, which read the current name, fall back to the previous one, and refuse to guess. `describe_ucm_layout` asks the same lookup before falling back to the legacy group report, so `--layout-only` keeps printing the classification. Conflict resolved in `config.py`: upstream's `chunk_size` field name, keeping this branch's `layout_only`, `ascend_soc` and `cuda_capability`. `tests/test_model_check_v2_api.py` pins the behaviour across the move: the internals are found in each shape they have lived in, a half-populated object is rejected, the renamed fields read both ways and refuse to guess, and window and state plans are never mistaken for full attention. 71 tests pass; isort, black and pyflakes clean. Not verified: nothing here has been run against the new upstream on a machine with vLLM. The adaptation follows the new source, and the loud failures are there so a remaining mismatch surfaces as an error rather than a pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Ascend and 5090 device-less runs both failed layout-only on
`UCMKVCacheSpec.scheduler_block_size`, which upstream removed on purpose
("Engine scheduler granularity is a parser diagnostic, not stored
metadata"). The line after it would have failed next:
`UCMKVCacheGroupSpec.tokens_per_block` became `block_size` plus a
`block_token_span` property that accounts for DCP sharding.
Report the scheduler block size only while a tree still exposes it,
rather than deriving a stand-in, and read the block span through an
accessor in the same shape as `spec_chunk_tokens` / `plan_restore_policy`.
Both failures were in the checker's own log path, so bulk and layerwise
passed on both platforms -- the device-less layers were never involved.
Also fold in the two substitutions the 5090 run needed on top of
b2de1f0. vLLM 0.30+ resolves its platform during `import vllm`, so the
launcher's refusal-when-already-resolved guard fired on every run.
Resolution order is a probe-timing question, not a layout one: clear the
cached answer, declare CUDA, and let the next read re-resolve. Children
need the same declaration, so `sim_bootstrap` grows a cuda branch beside
the Ascend one. Verified layout-neutral -- the normalized bulk and
layerwise logs are byte-identical to the pre-merge run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add VERIFICATION.md: what a device-less PASS does and does not assert, and what a run has to produce before its PASS may be used as evidence. The two claims a device-less run makes need separating, because they have different evidence and a fixed order. Faithfulness (is this the layout a real card produces?) comes from the real-card cross-check; correctness (does dump/load have a bug?) comes from the three-state black-box check, the stray-write check and fault injection. A device-less run can only substitute for a card on the second once the first holds -- two runs that both skipped the checks would agree perfectly. Also record the acceptance rules the runs already rely on but that lived only in commit messages: a zero exit status is not evidence (a missing __main__ guard once produced exit 0 with an empty log), the three lines whose presence means checks were skipped while still reporting PASS, why the pattern is capped at 250 so the third state is distinguishable, and why the transfer-range check has to precede poisoning. README had two claims that the 5090 and 910B runs have overtaken: cuda_sim is no longer unrun, and the platform-resolution change needs recording next to the substitutions it belongs with. State plainly that cuda_sim is verified for one model, and that the capability flag is asserted but its layout effect is untested -- the control that was meant to show it was inconclusive, since Qwen3-0.6B's layout does not branch on capability. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The A2 (910B3) and B2 (5090) service runs and both real-card cross-checks are done, so the criteria they were judged against need to match what the evidence actually supports. sim.json was listed first among the faithfulness criteria, which overstates it. It records vLLM's KVCacheConfig and nothing above it, so it cannot see the K/V split -- that lives in how a platform hands tensors to register_kv_caches. For Qwen3-0.6B the ascend_sim and cuda_sim snapshots are byte identical, while the layouts demonstrably differ (poisoned_blocks 448 vs 224). Within one platform the match is real evidence; across platforms it proves nothing, and it must not be reported as the headline. Say which criteria carry the platform-specific claim instead: the normalized logs, which include the tensor signature with shapes and strides, and the counters that differ by platform precisely because of the split. Record what each launcher has established on that basis: cuda_sim all four criteria, ascend_sim 1, 3 and 4 with no log diff yet, so its platform-specific claim rests on seven matching counters including the two the K/V split produces. Add the service criteria. Read the external hit from RequestOutput.num_cached_tokens: the v2 scheduler logs no hit line at all (get_num_new_matched_tokens returns without logging; "hit external" exists only in the legacy connectors), so a wrapper around the lookup is a logging convenience that proves nothing the field does not. It also costs VLLM_ENABLE_V1_MULTIPROCESSING=0, which leaves the production multiprocess path unexercised -- now listed as a gap, along with layerwise fault injection never having been done and only one model having been cross-checked end to end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ped range check as 0 The v2/legacy branch was chosen by sniffing the metadata for `requests`, so a future rename could route a v2 run away from the v2 checks. It is now read from kv_connector_module_path, and metadata of the other kind fails the run with a message that names the cause. transfer_ranges_checked could read 0 on a PASS when the check was skipped. It now reads "skipped (...)" with a PARTIALLY VERIFIED line, and a check that ran but found no ranges fails. _v2_batch reuses v2_worker_internals instead of its own lookup. Also applies isort/black to three files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ASS must not carry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
model-check 验证昇腾 KV 布局只能靠
ascend.py,必须有 NPU 卡;验证官方布局要么用cuda.py上 GPU,要么用cpu.py在 CPU 版 vLLM 上手写后端约束来模拟,后者跑的不是真实的 CUDA 注意力后端。另外,v2 connector 的比对有盲区。原来写入、比对、投毒用的地址都来自被测 connector 自己的
transfer_builder。connector 在 dump 和 load 时犯同一个错误(偏移算错,或者漏传某一层),写进去和读回来的位置是一致的,比对仍然通过。本 PR 做三件事:
ascend_sim.py和cuda_sim.py:没有 NPU / GPU 时,仍用 vllm-ascend / vLLM 原生代码生成各自平台的 KV 布局,KV 张量放在主机内存,照常跑 dump / load。verify()新增几项只依赖 vLLM 原生张量和 block 表的校验,不使用 connector 算出的地址。VERIFICATION.md,写清楚无卡 PASS 断言了什么、没断言什么,以及一次运行必须留下哪些证据。改动只在
toolkit/下的 model-check 和对应测试(另有cli.py补了__main__入口),vLLM、vllm-ascend、UCM connector 都没有改。model-check 在 vLLM 架构上的位置
绿色部分直接调用 vLLM / vllm-ascend 原函数,工具只决定传入的参数和运行设备:
EngineArgs(...).create_engine_config()load_format="dummy"、enforce_eager、max_num_seqs=2,挂上 UCMConnectorget_kv_cache_spec()initialize_model()在meta设备上构造kv_cache_utils.get_kv_cache_configs()num_gpu_blocks_override设为最少可用 block 数的 3 倍runner.initialize_kv_cache()get_scheduler_cls()→add_request()/schedule()evict_blocks(),让它走 UCM 外部命中KVConnectorFactory.create_connector()/register_kv_caches()上面任何一步报错,工具直接抛
UnsupportedEnvironment退出,不会改用自己的实现。橙色部分由工具负责:不启动 EngineCore / Worker,由
common.py直接创建 Runner 和 Scheduler;不跑前向,call_worker_layer_hooks()按注意力层执行时的顺序手工调用 connector 钩子(基线已有);无卡时的替换见下文。无卡启动器
原则:执行顺序、权重、计算 kernel、硬件探测可以替换;KV 规格、分组、形状、步长、block 编号、命中结果不动。两个启动器共用的部分放在
sim_runtime.py,避免两边各写一份。ascend_simcuda_simsynchronize、set_device改为空操作import vllm时就已解析平台,先清掉缓存结果再声明 CUDA--ascend-soc必须给出,并和安装包_build_info.__device_type__核对--cuda-capability必须给出,不猜get_arch/get_aicore_num按声明作答;自定义算子报告不可用;锁页内存报告不可用HostUvaBuffer换成 meta 替身(原需 91.5 GiB)create_device改为CpuDevice,UCM 以 simu 方式编译vLLM 会在新解释器里检查模型结构(
python -m vllm.model_executor.models.registry),猴子补丁传不过去。sim_bootstrap.py生成一个sitecustomize.py,让每个子解释器启动时带上同样的声明;它不依赖 torch / vLLM / UCM,并且会接上原有的sitecustomize。run_model_check.sh一条命令按 layout-only → bulk → layerwise 跑完一个模型。它以检查器自己打印的 PASS 行作为通过的依据,不看退出码:写这个脚本时发现cli.py缺__main__入口,python -m ucm_toolkit.cli什么都没跑就以 0 退出。verify() 流程
每个请求只调度一次,不跑前向,也不向 Scheduler 回填模型输出。
顺序上有三点不能调换,代码里有注释:
新增的校验
黑盒三态:目标块先全部写成
0xff,load 之后逐字节判定。这项检查不依赖各组的存储策略,滑动窗口组、Mamba 组都能直接覆盖。它只证明"没有放错位置",不证明"该恢复的都恢复了":整组都停在
0xff也能通过。所以每个组会单独标明有没有独立的必须恢复集合,没有的打印PARTIALLY VERIFIED。严格相等:对能从 vLLM spec 推出必须恢复哪些块的组(全注意力组的命中前缀、窗口恰好一块的窗口组的边界块),要求字节完全一致。其他几何结构在日志中标为 not natively assertable 并跳过。
地址合法性:允许写入的范围只从 vLLM 的 block 表和张量视图构建,再逐条核对 connector 报告的 load 地址;有任何一条越界就失败,并给出地址。
误写检查:load 前后对 KV 存储做快照,目标范围以外的字节不允许变化。
命中数:dump 的 chunk 数必须等于源请求调度范围按 chunk 粒度算出的值,load 的 chunk 数以目标请求长度为上界。两边各自独立计算,不从 connector 的 dump 计划里取期望值。
测试数据由
pattern_bytes()生成:取值 0~250,按(层, 分量, 块号)派生,多个尺度叠加,周期约 15.8 MB,远大于一个 KV block,块内任何错位都能看出来。最早的单尺度版本周期只有 251 字节,相隔 251 字节的两段互换是看不出来的。防止"跳过了检查却报 PASS"
这类问题比直接失败更危险,PR 里专门处理了几处:
spec/layout/transfer_builder移到了connector_worker,tokens_per_chunk改名chunk_size,attention_type改名restore_policy。v2_worker_internals()、spec_chunk_tokens()、plan_restore_policy()新旧名字都认,都找不到时直接报错,并说明该往哪个列表里加。kv_connector_module_path决定,不再从 metadata 形状推断。metadata 和配置不一致(比如字段又改名)时直接失败,并指出原因。transfer_ranges_checked=skipped (...),后面跟一行PARTIALLY VERIFIED,不会显示成 0;检查真正跑了却得到 0 个范围则直接失败。5f9861c7):误写检查的允许范围原来取自 connector 自己的地址;命中数的期望值原来从 connector 的 dump 计划推出;黑盒通过原来被当成"已恢复";cuda_sim原来把 fp8 / FA 版本支持强制设成 True,改变了后端选择。四处都已修正,tests/test_model_check_blind_spots.py有对应的回归测试。验证
无卡与真卡交叉核对
判据见
VERIFICATION.md:同一模型分别在无卡启动器和真卡上运行,比较 ①sim.json(vLLM 的KVCacheConfig)② 去掉时间戳等之后的日志 ③ 能从层数和块数推出来的计数 ④ 注入故障能否被发现并指出位置。①只在同一平台内有意义:Qwen3-0.6B 在ascend_sim和cuda_sim上的sim.json完全相同,但两个平台的布局实际不同,所以它不能单独作为结论。Qwen3-0.6B(1024 tokens,block size 128):
cuda_sim(capability 12.0)(30, 8, 128, 256)/(262144, 256, 2048, 1)ascend_sim(A2)cuda_simascend_simcompared_loaded_tensor_blockspoisoned_blocksstrict_oracle_blockstransfer_ranges_checked真实服务验证(真引擎、真卡)
同一个 prompt 通过
LLM.generate生成两次(temperature=0,固定 seed,enable_prefix_caching=False):Qwen3-0.6B 在 910B3(A2)和 RTX 5090 上都满足输出 token id 完全一致(64 个),第二次外部命中 1920 tokens(2008 token 的 prompt,2008 // 128 = 15个整块)。命中数读自 vLLM 的RequestOutput.num_cached_tokens。故障注入
手工修改
UCMTransferBuilder._build,让 dump 和 load 都少传一层:原有段校验仍然通过,新校验报错,并给出组、层、块号和"该字节仍是投毒值"。故障注入曾暴露过一次真实缺口:当时严格校验只覆盖了 11 个持久化组中的 1 个,已修复。DeepSeek-V4.1-Flash 在
vllm-ascend:v0.30.0rc1上 layout-only / bulk / layerwise 都通过过(12 组:1 MLA + 1 瞬态 ring + 10 滑动窗口,124 段,与真卡记录一致),但那是在合并新版 dev_connector 之前的版本上跑的,当前版本还没重跑。已知限制
PARTIALLY VERIFIED。VLLM_ENABLE_V1_MULTIPROCESSING=0,生产用的多进程路径没有覆盖到。ascend_sim还没做日志比对,它和真卡一致的结论依据是计数。--cuda-capability对布局的影响没有被证实:Qwen3-0.6B 在 9.0 和 12.0 下布局相同,这个对照说明不了问题。vllm-ascend:v0.30.0rc2的适配还没在服务器上重跑。使用
UCM 需以
PLATFORM未设置的方式编译(simu 运行时),并安装wrapt;编译方式、自检命令和每次运行要留存的证据目录见VERIFICATION.md。Test plan
cuda_sim对照 RTX 5090,判据 ①②③④ascend_sim对照 910B3,判据 ①③④ascend_sim补做日志比对(判据 ②)🤖 Generated with Claude Code