Repository navigation
[feature] support pp>1 for Kimi-K3 - #2365
Conversation
🏷️ CI GuideRuns automatically on every eligible PR before approval:
Heavy model tests:
|
845d7a4 to
02b4968
Compare
7cb6f65 to
56e20f9
Compare
56e20f9 to
dbaa979
Compare
5c811df to
501364c
Compare
|
Update: I went ahead and stacked the three PRs ( |
wanzhenchn
left a comment
There was a problem hiding this comment.
Review summary
Thanks for this PR. It adds pp>1 support for Kimi-K3, including DSpark speculative decoding under PP, the aux hidden-state relay, and KDA state and checkpoint handling. The approach looks right and the main fixes look sound.
- CI is green: black, ruff, and all 20 accuracy jobs.
- Unit tests pass locally at
501364cf4: 285 passed, 60 skipped.
Before merge, I'd like to see four things addressed:
- one latent bug,
- one assert path that needs confirmation,
- test coverage,
- the commit history and the PR description.
The performance items at the end are fine as follow-ups.
1. Correctness
1.1 [Must fix] PPAuxRelay mishandles the embedding tap (-1)
ATOM/atom/spec_decode/pp_aux_relay.py
Lines 45 to 47 in 501364c
ATOM/atom/spec_decode/pp_aux_relay.py
Lines 82 to 86 in 501364c
- What's wrong:
- On the first stage,
-1is added toown_ids.arm()then hookslayers[-1], which is the last decoder layer and aPPMissingLayeron that stage. - It also uses
spec.extract, whereas the base drafter resolves-1with_resolve_embeddingand_identity_extract. - The hook never fires, so downstream stages receive an all-zero buffer and nothing reports an error.
- On the first stage,
- Impact: K3's taps are [2, 23, 47, 71, 89], so this doesn't trigger today. But the relay is the generic DSpark path.
- Ask: Please mirror the base drafter's handling of
-1, or reject-1explicitly under PP.
1.2 [Please confirm] A fork landing on a spec-verify decode step hits an assert
ATOM/atom/model_ops/attentions/gdn_attn.py
Lines 1204 to 1207 in 501364c
- Concern:
- 838ac51 notes that under PP the checkpoint can land after the prompt, so a fork can reach decode. It only handles the non-spec KDA decode branch.
- With DSpark enabled, the first decode after the prompt is a spec-verify step. If that step carries a FORK checkpoint, this assert fires.
- Ask: Can this ordering happen under PP + DSpark + prefix caching? If it can, the spec branch needs handling. If it can't, please add a comment or a test that explains why.
1.3 [Minor] The PP path drops the producer's num_spec_step == 0 guard
- The description says the P/D producer keeps this assert, but the code only checks it on the non-PP path (
model_runner.pyL558-L567). - It's safe today, because the producer's scheduler sets
use_spec = has_spec and not is_pd_producer. - Please either keep the assert for producers on the PP path or adjust the description.
2. Tests
2.1 [Must fix] The new test doesn't run in CI
tests/test_verify_context_shift.py imports model_runner, which needs AITER, so pytest.importorskip skips it on the non-GPU runner. Consider moving verify_context_shift into a pure helper (e.g. in token_layout/decode.py) so it can be tested on CPU.
2.2 [Suggested] The new logic has no unit coverage
The existing is_deferred_out=False cases are all non-spec. CPU tests worth adding:
- Placeholder algebra for undeferred spec output: rejected counts from 0 to k, steps with no drafts, and the
max_tokensboundary. - The
_pp_checkpoint_holdlifecycle: a held sequence is skipped by both scan loops and is released on retire, preempt and abort. - The
PPAuxRelaysplit into incoming, own and outgoing taps for pp=2/4/8, using fake layers and including-1. has_state_forkon the decode path.
2.3 [Suggested] Add a PP accuracy CI case
Every fix here is PP-only, but all 20 accuracy jobs run TP8 or TP4. A Kimi-K3 pp2tp4 GSM8K job with DSpark and prefix caching would cover it.
3. Commit history and description
3.1 [Suggested] Bisectability
- 43f5aa7 removes the
NotImplementedErrorand enables spec decode under PP, but the following 10 commits are needed for correct output. - So any intermediate commit silently produces garbage with PP + DSpark. The commit messages themselves say "decodes noise" and "still broken".
- Please move the enabling change to the end of the series, or squash the chain into a few commits.
3.2 [Suggested] Stale references in commit messages
| Commit | Stale reference |
|---|---|
| 43f5aa7, d7fdb2e, 21034f8 | k3_pp2tp4/K3_PP2TP4.md (not in the repo) |
| 4547d54 | 9ba0d6a6 (pre-rebase SHA of 54b724f) |
| 6fc1fde | 0ac00173 (pre-rebase SHA of 21034f8) |
| 924147e | 3d7ff8a5 (not found) |
Please also:
- drop the debugging narrative ("not sufficient", "still broken", "decodes noise") from 43f5aa7 and d7fdb2e;
- fold "Trim the comments this PR adds" back into the commits it touches.
3.3 [Suggested] Update the PR description
- The GSM8K table predates the rebases, and its last three rows predate the verify-window fixes. Please re-run at the current head.
- ROCm/aiter#5783 merged on 2026-09-30, so the dependency note is out of date.
- There is no performance data. A pp2tp4 vs tp8 comparison of throughput and TPOT would help reviewers judge where PP decode is worth using.
4. Performance (fine as follow-ups)
4.1 The checkpoint hold is the largest cost.
- In our Kimi-K3 PP8 prefill runs,
_pp_checkpoint_holdaccounts for about 3.3 s of a roughly 7 s fixed prefill latency. - In #2709, which is stacked on this PR, b81cf24 and 7d55402 file a PAGE-backed checkpoint as soon as its chunk is scheduled, and keep the hold only for fork-class requests.
- Either note this as a known cost that #2709 addresses, or pull those two commits into this PR.
4.2 PP decode still requires --enforce-eager, so it has no CUDA graphs. This decides whether PP decode is practical. Please list it as follow-up work in the description.
4.3 Aux relay traffic grows with PP depth.
- Current cost: At PP8, the stage 6 → 7 boundary carries 4 taps. At hidden size 7168 in bf16, that is about 470 MB per 8192-token chunk. The
torch.catin pack plus theclonebefore the send copy the payload twice. - Alternative:
context_proj(weight [7168, 35840]) is linear over the concatenated taps. Each stage could apply its own weight slice and relay a single running sum in fp32. - Effect: Each boundary carries one fp32 column regardless of PP depth.
- Trade-offs: Each stage stores its own weight slice (about 103 MB per tap), and the summation order changes.
4.4 Small items:
- The per-row Python loop that stages draft columns in
prepare_input_ids(model_runner.pyL578) can be vectorized. _build_outputdoes two.cpu()syncs (model_runner.pyL3438-L3439); stacking the tensors makes it one.- The KDA fork state copy (
kimi_k3/model.pyL1352) copies every decode row; it could copy only the fork rows. Forks are rare, so the gain is small.
5. Checked, no issues
- The relay's async send is safe:
async_send_intermediate_tensorsclones beforeisend, so there is no race. - Enabling spec decode under PP doesn't affect P/D producers: see 1.3.
- K3 tap split at PP8 (12×7+9) is correct: each stage receives and forwards the expected taps.
- The MLA head padding in e39bcb6 (24 → 32 at TP4) is not a new cost: it is aiter's dispatch constraint, and TP8 already pads 12 → 16.
501364c to
70dc6d3
Compare
|
@wanzhenchn Thanks for the review. All points are addressed, except the performance items you marked as follow-ups. The PR is now one commit, 1. Correctness1.1 1.2 A fork on a spec-verify decode: it can happen, and it is now handled. Under PP + DSpark + prefix caching, three things combine:
The fix uses the same approach as the plain decode path:
1.3 Producer guard on the PP path: fixed in code. 2. Tests2.1 The new test now runs in CI. 2.2 Added:
2.3 Added the CI case 3. History and description3.1, 3.2 Squashed into a single commit. This removes the bisect hazard, the stale references, the debugging narrative and the separate "Trim the comments" commit in one step. 3.3 Description updated:
4. PerformanceLeft as follow-ups and listed under "Known limits" in the description: the hold's prefill cost and #2709's fix for it (4.1), |
65ebed6 to
d102bb5
Compare
d102bb5 to
d064068
Compare
d064068 to
0f4d211
Compare
Drafter.take_relayed_aux keeps the received [N, k*H] block and aux_for serves it ahead of the own-tap buffers, so the last stage no longer splits it per tap for compute_draft_kv to concatenate back (DSpark skips the cat when it is the only block). The relay takes the drafter class's spec on every stage instead of the drafter's private _aux_capture_spec, and the runner no longer hands it _aux_buffers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_aux_capture_spec now parses dspark_target_layer_ids once, through aux_capture_spec, and drops its unused target_model parameter. PPAuxRelay keeps only spec (no layer_ids / hidden_size copies), drops the dead _armed guard, and raises the missing-payload error from one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@valarLip The review fixes are pushed in
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
valar-cop
left a comment
There was a problem hiding this comment.
LGTM
ci Run non-GPU unit tests (Pre Checkin) failed — main is already broken the same way (high): The attached log is empty (0 bytes), but the same job fails 4x on main in the same window, and the author identifies the abort as collection of tests/model_ops/test_dp_gather_routed_rows.py (added by #2776, untouched here) on a missing triton.language.extra. Not caused by this PR.
same job on main: ['2026-11-03T14:52 failure', '2026-11-03T14:40 failure', '2026-11-03T14:39 failure', '2026-11-03T14:17 failure', ...]
Collection stops attests/model_ops/test_dp_gather_routed_rows.pywithModuleNotFoundError: No module named 'triton.language.extra', before any test runs
next: Wait for the #2954 stub fix on main, merge main into the branch and re-run; this also unblocks the gated GPU accuracy jobs. https://github.com/ROCm/ATOM/actions/runs/41663095846/job/126011160276
ci Atomesh Accuracy Validation failed — ci script/config bug, not the pr (high): The workflow triggers on .github/benchmark/models_accuracy.json (touched by this PR) but on a pull_request its whitelist selects zero models, so models_json is [] and the matrix include expands to nothing; unlike atom-test.yaml there is no placeholder fallback. Kimi-K3 is not in atomesh_levels, so no atomesh job can exist for this change.
include: ${{ fromJson(needs.load-test-models.outputs.models_json) }}
include: ${{ fromJson(needs.load-test-models.outputs.models_json || '[{"runner":"ubuntu-latest","model_name":"skip"}]') }}
next: Add atom-test's placeholder-entry fallback to the atomesh matrix (or skip the workflow on catalog-only PRs), then rerun. https://github.com/ROCm/ATOM/actions/runs/41663095656
Brings #2365's fifteen review commits since c4975cd (2edbab7 through 30829a3). One conflict, in `Scheduler`'s PP state-checkpoint hold. #2365 turned `_pp_checkpoint_hold` into `_checkpoint_holds`, keyed by the holding chunk's end so only that chunk releases it; this branch files the checkpoint at schedule time where the class allows it and skips a rung at prompt end. Keep #2365's map and release, and this branch's conditions for taking a hold. The note that this branch would file at schedule time is dropped, as the merge does so. Co-authored-by: Cursor <cursoragent@cursor.com>
ganyi1996ppo
left a comment
There was a problem hiding this comment.
Thanks for the round of fixes. I re-read at 337bcd5 (merged with main a8946ff). All 15 earlier points are addressed. The affected unit tests show no regressions against the merge base: the one failure, test_model_runner_exit.py::test_a_failed_megamoe_close_still_releases_everything, fails at the merge base too.
Worth addressing
- The last stage still allocates an aux capture buffer for every tap, including the ones that now arrive through the relay (inline on
atom/spec_decode/drafter.py:455).
| spec = self._aux_capture_spec() | ||
| if spec is None: | ||
| return | ||
| self._aux_buffers = [ |
There was a problem hiding this comment.
arm_aux_capture allocates one [max_num_batched_tokens, hidden_size] buffer for each id in spec.layer_ids. Under PP, the taps owned by earlier stages hook a PPMissingLayer that never fires, and aux_for substitutes _relayed_aux for them, so those buffers are never written or read.
For Kimi-K3 at pp2, the taps are [2, 23, 47, 71, 89] and stage 0 holds layers 0-46. That leaves 2 idle buffers on the last stage: 2 x 16384 x 7168 x bf16, about 470 MB per rank taken from its KV-cache budget. The waste grows with PP depth: 4 idle taps, about 940 MB, at PP8.
Could arm_aux_capture allocate (and hook) only the taps resident on this stage? PPAuxRelay already computes incoming_ids.
There was a problem hiding this comment.
Fixed in a1436cd: arm_aux_capture now gives taps that sit on a PPMissingLayer (relayed from an earlier stage) a 0-row buffer and no hook, so only this stage's own taps take memory. Covered by test_last_stage_arms_only_its_own_taps.
Taps owned by an earlier PP stage arrive through the relay, so the last stage gives them a 0-row buffer and no hook (~470 MB/rank for K3 at pp2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
valar-cop
left a comment
There was a problem hiding this comment.
a few small things..
ci Atomesh Accuracy Validation failed — ci script/config bug, not the pr (high): The workflow triggers on .github/benchmark/models_accuracy.json (touched by this PR), but on a pull_request its whitelist selects zero models, so models_json is [] and the atomesh-test matrix include expands to nothing; unlike atom-test.yaml there is no placeholder fallback and GitHub fails an empty matrix. Kimi-K3 is not in atomesh_levels.
include: ${{ fromJson(needs.load-test-models.outputs.models_json) }} # atomesh-accuracy-validation.yaml:138, no fallback
include: ${{ fromJson(needs.load-test-models.outputs.models_json || '[{"runner":"ubuntu-latest","model_name":"skip"}]') }} # atom-test.yaml:265
atomesh_levels = {"DeepSeek-R1-0528": "main", "gpt-oss-120b": "nightly",}
next: Not a PR defect: add atom-test's placeholder-entry fallback to the atomesh matrix (or skip the workflow on catalog-only PRs), then rerun; no job log was attached. https://github.com/ROCm/ATOM/actions/runs/41762102572
| def relayed(lid): | ||
| # On an earlier PP stage: `take_relayed_aux` serves it instead. | ||
| # Out-of-range ids are left for install_aux_taps to reject. | ||
| if lid == -1: |
There was a problem hiding this comment.
relayed() uses its own residency rule while pp_aux_relay uses is_first_rank, can we share one helper?
There was a problem hiding this comment.
Done in b62bf7a: drafter.tap_is_resident is the one rule now (is the tap's module a PPMissingLayer, the embedding included), and both arm_aux_capture and PPAuxRelay use it.
Both now ask whether the tap's module is a PPMissingLayer, the embedding included, instead of the relay keying the -1 tap on is_first_rank. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
valar-cop
left a comment
There was a problem hiding this comment.
LGTM
ci Atomesh Accuracy Validation failed — ci script/config bug, not the pr (high): The workflow triggers on .github/benchmark/models_accuracy.json, which this PR touches, but on a pull_request its whitelist selects zero models, so models_json is [] and the atomesh matrix include expands to nothing. Unlike atom-test.yaml this workflow has no placeholder fallback, and GitHub fails a job whose matrix resolves empty. Kimi-K3 is not in atomesh_levels, so no atomesh job can exist for this change.
include: ${{ fromJson(needs.load-test-models.outputs.models_json) }} # atomesh-accuracy-validation.yaml:138, no fallback include: ${{ fromJson(needs.load-test-models.outputs.models_json || '[{"runner":"ubuntu-latest","model_name":"skip"}]') }} # atom-test.yaml:265, with fallback filtered = [m for m in models if atomesh_levels.get(m["model_name"], "skip") in allowed] # atomesh_levels has no Kimi-K3 entry
next: Not a PR defect: add atom-test's placeholder-entry fallback to the atomesh matrix (or skip the workflow on catalog-only PRs), then rerun. No job log was attached to this run. https://github.com/ROCm/ATOM/actions/runs/41766392543
|
@valarLip would you review again at the latest head? |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2365 has landed on main as the squash 64412f1, so this PR now targets main. 64412f1's tree is #2365's head 9fb45ff, which this branch already merges, merged with main d8525dd (#2913). The only new change this merge brings is #2913. scheduler.py and tests/test_pp.py conflicted only because the squash repeats #2365's changes that this branch edits further. They keep this branch's versions: the result is this branch merged with d8525dd, which merges cleanly, and its diff against main is this PR's diff against 9fb45ff unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
main's ROCm#2365 brings PP > 1 to Kimi-K3, so 'such an engine never verifies' no longer holds. The refusal stands: PP rings forward_vars per in-flight step while bind_target binds the draft to one slot's filters. Message, comment and guide row now say that. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tn now reads main's ROCm#2365 has GDNStateMixin._init_gdn_state read model_runner.num_spec_tokens (every PP stage runs the verify window), but this GPU-only test's ModelRunner stub never set it, so all 19 cases fail with AttributeError before reaching what they test. The stub sets it to num_spec, the value its drafter stub already carries as mtp_k. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A P/D producer never drafts, but it must write the draft's context K/V for the prompt, which the decode drafts from. Under PP4 the drafter is built on the last stage only: - Its taps (layers 3/17/31/45/59) mostly run on earlier stages; main's `PPAuxRelay` (#2365) carries DSpark's to the last stage. A PP drafter that taps target layers without a config-only aux spec (EAGLE3) is refused at startup: nothing relays its taps, and a producer would write wrong context K/V. - A block drafter warms up on at least two sequences. That floor is read off the config, not the stage's drafter, so every stage warms up on the rows the last stage does. - The draft pool's units say how they split by KV head (DSpark's binding reads and writes page-16: eight runs a block), so a TP1 stage's draft K/V is cut per decode rank like the target's. `merge_pages` numbers regions appended after a mapped contributor from the consumer's end, where the consumer appends them too; the producer resolves them against the consumer's region count at handshake. - Any P/D producer whose drafter writes its context K/V apart from propose (DSpark: M3.1, K3, V4.1) skips `propose()` and its draft graph warmup, on the deferred output path as on the direct one, PP or not, and leaves the DP draft lockstep (every rank skips alike). Its drafts were discarded anyway (`mtp_k` 0). EAGLE still proposes, since it leaves a final chunk's context K/V to propose's first step. - So the M3.1 draft may run below TP4 on a producer. Its one-KV-head check runs at the top of the block pass, before anything is written. The last stage's embedding is a placeholder, so the vocab check reads the LM head. - Adds a recipe section with the prefill at `--max-num-batched-tokens 8192`: the sparse prefill launches one `pa_fwd` row per (token, KV head), HIP caps a launch at 65535 rows, and a TP1 stage holds all four KV heads. Co-authored-by: Mengqing Cao <cmq0113@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Motivation
Kimi-K3 only ran at TP8. This PR makes it serve at
--pipeline-parallel-size 2 --tensor-parallel-size 4on MI355X ×8, covering plain decode, prefix caching (KDA state checkpoints), LMCache offload and DSpark speculative decoding. None of these paths had been exercised under PP for this model.The first commit's message lists every original change; the
review:commits on top each address one review point.Technical Details
Model and transport
block_residual(AttnRes) now crosses the stage boundary with the hidden states. It used to be dropped there, and stage 1 raisedKeyErrorduring warmup.embed_tokensis aPPMissingLayer.PPAuxRelay(atom/spec_decode/pp_aux_relay.py) relays DSpark's aux hidden states across stages. K3 taps layers 2 and 23, which live on stage 0, so without it the drafter read zero buffers. The embedding tap (-1) resolves asDrafter.arm_aux_captureresolves it.Speculative decoding under PP
A PP engine now verifies its own drafts. PP output is undeferred, so:
postprocessleaves exactly one run ofmtp_k + 1placeholders after every step, whatever was rejected.The attention backends rebuild the verify window from
tokenIDProcessor.verify_ctx_lens:num_rejected(unchanged);The scheduler stages the undeferred window from the recorded
seq.num_placeholder_tokens.The verify window is gated and sized on the run's speculative config, not on whether this stage drafts. Stage 0 used to allocate 3 conv rows per KDA slot while the kernel addressed 10.
A P/D producer still never speculates, with or without PP. Under PP a producer keeps any drafter; the other architectures and spec methods allowed under PP are declared in
registry.pp_speculative_methods.TBO is refused under PP (
UBatchWrapper.forwardtakes nointermediate_tensors).On the last stage the drafter reads the relayed aux block in place (
Drafter.take_relayed_aux), with no per-tap split and concatenation.KDA state and prefix caching under PP
GDNStateMixin.spec_honours_state_fork, which K3 sets). Every other GDN backend keeps the assert.CI
Kimi-K3 DSpark PP2, runs pp2 × tp4 with DSpark, prefix caching and ptpc_fp8 online quant on. It is the only accuracy job that reaches the PP paths.Known limits (follow-ups)
--enforce-eager: PP decode has no CUDA graphs yet. This is the existingforward_varsring assert, not something this PR adds. CUDA-graph support for PP decode is follow-up work.context_projpartial sum would keep it at one fp32 column.state_offload.py,CacheEngineKeyhas no PP component).Dependency
a8w8_bpreshufflerows for K3's KDA gate at TP4, N=3072/K=128) merged on 2026-09-30.Test Plan
Unit tests
python -m pytest <file>for every file undertests/, one process per file. Compared withmainat the same commit, on the same host.test_verify_ctx_lens.py: the verify window's positions, deferred and undeferred. It importsmodel_runner(wheretokenIDProcessor.verify_ctx_lenslives, per @valarLip's review), so it needs aiter and skips on the non-GPU runner.test_pp_draft_input_ids.py: the vectorized PP verify-draft scattertest_pp_aux_relay.py: tap split at pp = 2/4/8, the-1tap, the round trip through every stage, the send buffer sized per forward, and the last stage arming only its own tapstest_gdn_state_fork.py: a fork on the decode and spec pathstest_scheduler_mtp_max_tokens.pytest_pp.py, including the flush on the ack pathmodels/test_kimi_k3_mono_plan.pyAccuracy
lm_evalas inrecipes/Kimi-K3.md,num_concurrent=32.-tp 4 --pipeline-parallel-size 2 --enforce-eager --block-size 128 --enable_prefix_caching --method dspark --num-speculative-tokens 7 --max-num-seqs 64, plus the recipe's ptpc_fp8 online quant.rocm/atom-dev:nightly_202610251529, with its own aiter.Test Result
Unit tests
5ab3d68f7(this branch merged withmain99ebf633a), every test file was run, one process per file, againstmainitself in the same container. No file does worse. At headb62bf7ac6, every file the PR affects was re-run against the merge basea8946ff73: no regressions (the one failure,test_model_runner_exit::test_a_failed_megamoe_close_still_releases_everything, fails at the merge base too). Every file that differs does so by gaining tests:test_verify_ctx_lens(26),test_pp_aux_relay(24),test_pp_draft_input_ids(1),test_gdn_state_fork(5);test_pp+7,test_scheduler_mtp_max_tokens+6,models/test_kimi_k3_mono_plan+2.test_verify_ctx_lens,test_pp_aux_relayandtest_gdn_state_forkimport aiter, so the non-GPU pre-checkin runner skips them; they pass in therocm/atom-devnightly image.GSM8K, pp2 × tp4, DSpark, prefix caching, ptpc_fp8, 1319 questions
f34cb8a8e, nightly_202610310756main713eb0614(70dc6d37c), nightly_202610251529mainCI, TP8,Kimi-K3 DSparkpp2 × tp4 scores the same as
main's TP8 DSpark case from the same week, and its acceptance matches TP8 (53.7–55.2% on earlier same-host runs). Earlier numbers for this PR (0.9666 on nightly_202609220547) were measured on an older image, so they are not comparable.GSM8K, pp2 × tp4, no DSpark, prefix caching + LMCache offload, ptpc_fp8, 1319 questions
flexible 0.9674 ± 0.0049 / strict 0.9682 ± 0.0048 (
e1c80ceae, nightly_202610310756)Submission Checklist