Repository navigation
feat(atom): optimize atom pd performance - #209
Merged
Merged
Conversation
ChengYao-amd
marked this pull request as ready for review
October 22, 2026 02:51
ChengYao-amd
requested review from
jiejingzhangamd,
limou102 and
xiaobochen-amd
as code owners
October 22, 2026 02:51
Both ATOM API processes spent ~45% of the wall clock in stop-the-world gen-2 GC passes that grew with every completed request (~0.7 s per TTFT after an hour of AgentX C64). - ATOM patch: finished sequences are no longer queued on CoreManager.outputs_queue when serving online (nothing reads it there), and the per-request context gauges are opt-in (ATOM_METRICS_PER_REQUEST_CONTEXT=1). - Router: the frontend /metrics no longer scrapes engines it reads nothing from (ATOM), halving the /metrics renders of each ATOM API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
…knob - ATOM patch: the sparse indexer's DCP top-k candidate exchange (24 gathers per decode step, ~1 ms) goes through dcp_all_gather, which picks aiter's IPC all-gather instead of pynccl. - Kit: MTP_EXPERTS=mxfp4 quantizes the MTP layer's routed experts online to MXFP4 (they are BF16 in the checkpoint). Default bf16 keeps the previous online quant config byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
…ove 8 Default 4 keeps the previous 34 sizes at CONC 64. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
aiter's gfx950 configs for the MLA q/v up-projection batched GEMM use BLOCK_SIZE_M 64 from M_LEQ_64 up, so every decode M that is not a multiple of 64 (graph 36/40/44, drafts from 36) takes the masked path at 2-2.5x the time. One bucket per decode M with the sweep's fastest config (MI355X, output-checked): ~1.4 ms per decode step at graph 40. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
The DCP decode merge runs one all_to_all_single per layer on RCCL (81 per GLM-5.2 step, ~3.3 ms), and no RCCL setting helps. - aiter patch: alltoall_vec next to allgather_vec on the custom all-reduce buffers, reached through dispatchAllGather's gather_dim (-1000) so the registered-buffer and CUDA-graph paths apply as they are; CustomAllreduce.custom_all_to_all; module rebuilt at build time. - ATOM patch: the DCP merge uses it when it applies, RCCL otherwise. 4 x MI355X, bit-identical to RCCL: 5.3 MB/rank 56 -> 34 us, 1.3 MB/rank 32 -> 13 us. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
aiter's GLM-5.2 a8w8_bpreshuffle table holds M = 64/128/256, so the MTP verify at graph >= 36 (M = 144 ...) runs padded or default FlyDSL kernels. Tuned M 144..240 for the nine GLM-5.2 shapes and kept the 27 rows at least 5% faster than the production path (e.g. qkv_a at M 160 14.9 -> 11.5 us), as an extra model_configs table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
Both ATOM legs rendered the chat template and tokenized the same prompt (~0.25 s each at 130K tokens). The prefill leg now asks for return_token_ids and the router passes them in the decode's kv_transfer_params['prompt_token_ids'], which ATOM takes as the prompt (and strips before the engine). A prefill that returns no ids leaves decode tokenizing as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
Validated on the 4P4D kit: ~1 ms less per decode step, GSM8K 0.990 and real MTP acceptance 0.780 (BF16: 0.975-0.985, 0.761). MTP_EXPERTS=bf16 keeps the checkpoint's BF16 experts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
…mbine + FP8 quant) With the quantized combine (PBM + per-token FP8 o_proj input), the DCP decode merge ran pack -> custom all-to-all -> unpack/combine/quant. A new aiter kernel reads this rank's heads straight from the peers' registered buffers, merges them with the LSE weights and writes the FP8 rows and scales, so the received copy and the unpack launch go away. The pack writes a split layout (data, then fp32 LSE) for 16-byte loads. Matches the RCCL path to FP8 rounding (4.6e-6 of codes differ); merge microbenchmark at 160 rows 44.7 -> 38.1 us per layer; decode step -0.2..-0.6 ms (B16-64); GSM8K 0.980. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
…de container Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
…skips ATOM The Rust router side of two Python router changes (f7e46219, 91910e57): - protocol.rs: the ATOM prefill leg asks for return_token_ids, and the hand-off passes the returned prompt ids to the decode in kv_transfer_params['prompt_token_ids'], so ATOM's decode skips rendering the chat template and tokenizing the same prompt (~0.25 s at 130K tokens). A reply without ids leaves the decode tokenizing as before. - handlers.rs / metrics.rs: the frontend /metrics scrapes only engines it reads something from (sglang, vllm). An ATOM scrape only cost its API process a /metrics render on top of the client's own scrapes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
…4045 settings The kit's decode kept its own settings and its CPU tier was LMCache; this takes the rest of ATOM's GLM-5.2 AgentX recipe and InferenceX PR #4045's 1P1D: - decode: mem util 0.94, --max-num-batched-tokens 2048 (DECODE_MAX_BATCHED_TOKENS) and AITER_REUSE_IDENTICAL_COMM_GROUPS=1, ~196K KV blocks instead of ~156K, which moves the AgentX decode-prefix cliff from about C85 to C105. - MC_NUM_QP_PER_EP=1 on every Mooncake peer and ATOM's default 16 send workers (PREFILL_SEND_WORKERS empty; 4 stays the fallback of gotcha 13). - PREFILL_CPU_TIER=store (default): engine/store.sh starts a Mooncake Store master and one 768 GiB owner per NUMA node on the prefill's rails, backed by THP without root; the prefill offloads to it with one load worker. lmcache keeps the previous LMCache tier, off drops the CPU tier. - TOPK_FORCE_PATH=one and --timeout-keep-alive 900 on both legs. - ROUTER_BACKEND defaults to rust. Graph sizes, max-num-seqs and the MXFP4 MTP experts stay the kit's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
…to HBM do not land Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
… kit
Patches (deploy/docker/patches/atom_nightly/):
- patch_aiter_custom_alltoall_combine.py is merged into
patch_aiter_custom_alltoall.py: one set of edits and one rebuild of
module_custom_all_reduce, which now always clears the JIT build dir. The
combine patch anchored on text the other one inserted, so the build
depended on the glob order (bash under en_US.UTF-8 sorts _combine first).
The patched sources are as before, plus the next item.
- Both Python entries refuse world sizes other than 2, 4 and 8, the only
ones the kernels are instantiated for (should_custom_ag allows 6).
- patch_aiter_glm52_decode_gemm_rows.py fails the build when a table aiter
merges it with already holds one of its keys; aiter would raise at engine
start instead.
- check_dcp_merge.py: GPU check of both kernels against RCCL (bit-exact
all-to-all, merge up to FP8 rounding, eager and from a CUDA graph), to
run after a base move.
- README.md and patch.upstream.status.md list the new patches with their
contract, drop conditions and upstream status (none filed yet), and the
per-request context gauges the GC fix turns off.
- ruff format (0.13.0) on the PR's Python files; the bmm TUNED table stays
compact inside fmt: off.
Router: the Python ATOM hand-off leaves out an empty or negative prompt-id
list, as the Rust one does; token_ids.py no longer says ATOM lacks
return_token_ids.
4P4D kit:
- DECODE_GRAPH_STEP is gone (no gain measured; 0 looped forever), and
PREFILL_EXTRA_ENV joins DECODE_EXTRA_ENV, e.g. for the gauges.
- The JIT cache volume name carries the leg's --max-num-batched-tokens,
which ATOM's compile cache does not key on.
- up.sh refuses a decode budget below (MTP_K + 1) x 2 x CONC; store.sh
checks THP and each NUMA node's free memory before starting the owners.
- README and the manual walkthrough describe the new defaults: validation
and reference numbers with the Store, gotchas 8, 11, 13 and 14, the DCP
prefill's validated command with PREFILL_CPU_TIER=lmcache, max-num-seqs
among the settings not taken from PR #4045, no machine names.
Checked: pytest tests/unit/{router,common,server,engine} 909 passed; ruff
0.13.0 format and check; shellcheck; all nightly patches applied to
rocm/atom-dev:nightly_202610131505 in both glob orders (a second run is a
no-op, the module rebuilds); check_dcp_merge.py passes on 4 MI300X GPUs and
fails with a zero tolerance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
InferenceX does not allow MXFP4 for the MTP draft, so the MTP layer 78 stays BF16 as in the checkpoint and MTP_EXPERTS goes away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
C56-C112 re-run on the Store after dropping the MXFP4 MTP experts: 1.4-2.5% below the MXFP4 runs (longer decode steps, ~198K instead of ~203K KV blocks) and still 5.0-23.8% above InferenceX PR #4045. GSM8K 0.975 with real acceptance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
…s as Mooncake offload Second round of review follow-ups at 41bd647. 4P4D kit: - up.sh refuses PREFILL_PP=1 with PREFILL_CPU_TIER=store. ATOM's Mooncake Store offload rejects decode context parallelism at startup (kv_transfer/offload/mooncake_store/config.py), so the DCP prefill died after store.sh had mounted 2 x 768 GiB. env.sh, the README and the manual now say the DCP prefill takes the LMCache tier. - agentx.sh writes the AgentX offload metadata from PREFILL_CPU_TIER. It only looked at PREFILL_OFFLOAD_GB, so Store runs (the default) were labelled LMCache and PREFILL_CPU_TIER=off runs DRAM offload. The Store is reported as KV_OFFLOAD_BACKEND=mooncake with mooncake_master's version, as in InferenceX PR #4045's results. - Numbers after dropping the MXFP4 MTP experts: ~198.5K KV blocks (+24%), the decode-prefix cliff at about C100, and the manual's C56-C112 throughput (32925 / 40075 / 46234 / 48154). The README no longer quotes the MXFP4 MTP runs; the LMCache comparison is labelled as an earlier image and kit. Patches: check_dcp_merge.py runs on 4 or 8 GPUs only. On 2 GPUs a rank merges 32 of GLM-5.2's 64 heads, more than the one-kernel merge takes, so its merge cases failed by construction. Router: comments say ATOM returns the prompt ids at the top level for completions too. Checked: up.sh dry run with a stub docker (PREFILL_PP=1 with the Store refused; with lmcache or off, and PREFILL_PP=4, past the check); the agentx.sh offload lines for every tier pass InferenceX 918524ff's aggregation checks; check_dcp_merge.py exits on 2 ranks before touching a GPU; pre-commit on the changed files, shellcheck, cargo test and clippy, pytest of the touched router tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
ChengYao-amd
force-pushed
the
dev/yaoc/atom-pr4045-opt-20261020
branch
from
October 22, 2026 06:55
96a3af8 to
3424bf4
Compare
…rashing `cargo test` has died with SIGSEGV on every CI run since ubuntu-latest moved to Ubuntu 26.04. gcc 15 there defaults to -march=x86-64-v3, so libzmq's copy of its 64-byte-aligned command_t becomes a `vmovdqa ymm` store into a queue chunk that zeromq-src allocates with plain malloc (16-byte aligned, because it never defines HAVE_POSIX_MEMALIGN). It faults on the first zmq_connect, i.e. in the kv_event subscriber tests. Ubuntu 24.04's gcc 13 targets plain x86-64 and never emitted the aligned AVX store, which hid the bug. Define HAVE_POSIX_MEMALIGN (and the cache-line size it aligns to) for the x86_64 Linux C++ build through rust/.cargo/config.toml, which `cd rust && cargo build` in the Dockerfiles also picks up. Verified on the ubuntu-26.04 runner: fmt, clippy and the full `cargo test` pass 10 times out of 10 (10/10 crashed before); 24.04 is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
The agent_sys job has failed 24 env_mgr tests on every CI run since ubuntu-latest moved to Ubuntu 26.04, whose runner image ships /usr/bin/bwrap (24.04 did not). `select` prefers bubblewrap whenever it is on PATH, and bubblewrap confines by exec: `apply` is a no-op in-process, and an ungranted path inside the bwrap'd child lands on bwrap's tmpfs instead of returning EACCES. Every denial those tests assert is Landlock's, so they ran against the wrong mechanism. The sandbox itself is not weakened. - conftest: an autouse fixture reports bwrap as absent to `select` when the kernel also has Landlock; `run_confined` reads the probe at call time so its forked child sees the same answer. `availability` still reports the real machine, and bubblewrap's branch keeps its fake-binary tests. - test_wrap_argv_refuses_when_the_binary_vanished: after deleting the fake bwrap, PATH is only the emptied directory, not the original PATH that may hold a real bwrap. Reproduced in an ubuntu:26.04 container with bubblewrap: the 24 CI failures all pass after this; without bwrap the full suite is unchanged (2436 passed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
With `cargo test` no longer crashing, the rust job on Ubuntu 26.04 now reaches the dsv4 parity step and fails: "sglang is installed but has no dsv4 encoder". The step used the runner's system Python, which is 3.14 there. sglang 0.5.20+ ships only cp310-cp313 wheels, so `pip install sglang` falls back to 0.5.9, the last release with a pure-Python wheel or sdist, from before the dsv4 encoder existed. 24.04's Python 3.12 got the current release. Pin 3.12 with setup-python for this step (0.5.22 dropped cp310, so the 3.10 the other jobs use would not do either). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: YaoCheng <Cheng.Yao2@amd.com>
xiaobochen-amd
approved these changes
Oct 23, 2026
This was referenced Oct 23, 2026
llying-001
added a commit
that referenced
this pull request
Oct 23, 2026
…compares with our 4+4 C80 (ATOM +25% at equal interactivity) Signed-off-by: liyingli <liyingli@amd.com> Co-authored-by: Cursor <cursoragent@cursor.com>
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.
This PR has three parts:
/metricsno longer scrapes ATOM workers.Result: with the new kit defaults, C56–C112 run 5.0%–23.8% above InferenceX PR #4045, and the peak moves from 42.5K at C96 to 48.2K tok/s/GPU at C112.
1. ATOM / aiter patches (
deploy/docker/patches/atom_nightly/)These apply only with
ATOM_NIGHTLY=1(rocm/atom-dev:nightly_202610131505: ATOM 661ef766, aiter 9205e1059). Like the existing scripts there, they are strict: a drifted anchor fails the build.patch_aiter_custom_alltoall.pyadds both aiter kernels below and rebuildsmodule_custom_all_reduceonce, after clearing its JIT build directory.check_dcp_merge.pychecks both kernels against RCCL on GPUs (bit-exact all-to-all, merge up to FP8 rounding, eager and inside a CUDA graph); run it on 4 or 8 GPUs after moving the base image.patch_atom_api_gc_stalls.pyCoreManager.outputs_queue(nothing reads it online), and the per-request context gauges are opt-in (ATOM_METRICS_PER_REQUEST_CONTEXT=1)patch_atom_dcp_topk_custom_ag.pydcp_all_gather(aiter's IPC all-gather instead of pynccl)patch_aiter_mla_bmm_configs.pypatch_aiter_custom_alltoall.py(first half) +patch_atom_dcp_custom_a2a.pyalltoall_vecon the custom all-reduce's registered buffers and CUDA-graph path; the DCP merge uses it when it applies and RCCL otherwisepatch_aiter_glm52_decode_gemm_rows.pymodel_configstable; the build fails if aiter's tables already hold one of its keyspatch_aiter_custom_alltoall.py(second half) +patch_atom_dcp_custom_a2a_combine.pyalltoall_combine_quant: reads this rank's heads straight from the peers' registered buffers, merges them with the LSE weights and quantizes per token to FP8, so the received copy and the unpack launch go awayBoth Python entries of the all-to-all accept world sizes 2, 4 and 8 only, the sizes the kernels are instantiated for.
In total, at B40 with ~101K context, the patches in the first five rows take a decode step from 42.77 ms to about 40 ms (−6…−7%), and the one-kernel merge takes off another 0.2–0.6 ms.
2. Router (
infera/,rust/router/)return_token_ids, and the hand-off puts the returned ids in the decode'skv_transfer_params['prompt_token_ids']./metricsno longer scrapes ATOM workers (Python and Rust): the frontend reads nothing from ATOM's exposition, so each scrape only cost ATOM's API process one more/metricsrender. sglang and vllm are unchanged.3. GLM-5.2 4P4D kit (
examples/atom_4p4d_glm5.2/)--max-num-batched-tokens 2048(DECODE_MAX_BATCHED_TOKENS) andAITER_REUSE_IDENTICAL_COMM_GROUPS=1.MC_NUM_QP_PER_EP=1on every Mooncake peer (P, D and the Store), and ATOM's default 16 send workers (PREFILL_SEND_WORKERSis empty by default). With two QPs, 16 workers used to hang an RDMA write; with one QP the hang did not recur in 10 deployments.PREFILL_CPU_TIERisstore(default),lmcacheoroff.store, the newengine/store.shstarts a master and two owners, one per NUMA node withSTORE_OWNER_GB=768GiB each, backed by 2 MiB pages through glibc's THP malloc (no root needed).store.shfirst checks THP and the free memory of both NUMA nodes, andup.shchecks that the decode's batch budget covers (K+1) × 2 × CONC tokens.PREFILL_PP=1) cannot use the Store: ATOM's Store offload refuses DCP, soup.shstops that combination and the DCP prefill runs withPREFILL_CPU_TIER=lmcache.TOPK_FORCE_PATH=oneand--timeout-keep-alive 900on both legs. The Rust router reuses idle connections, and ATOM's 5 s default closes them under it.ROUTER_BACKENDnow defaults torust.<CTR>-cache-<role>-<image-id>-mbt<N>), because ATOM's compile cache is not.agentx.shrecords the CPU tier in the AgentX metadata:KV_OFFLOAD_BACKEND=mooncakefor the Store,lmcachefor LMCache, no offload foroff.PREFILL_EXTRA_ENV,DECODE_EXTRA_ENV,DECODE_MAX_BATCHED_TOKENS,PREFILL_CPU_TIER,STORE_OWNER_GB,STORE_OWNER_DEVICES, and the Store ports.OFFLOAD_PROFILE=1(logging only).4. Behavior changes / compatibility
ATOM_NIGHTLY=1image carries 7 more patchesatom:{prefill,decode}_request_context_tokensare no longer recorded by defaultATOM_METRICS_PER_REQUEST_CONTEXT=1when you need them (for example, for profiling)return_token_ids: true; the decode'skv_transfer_paramsgetsprompt_token_ids. ATOM's request models ignore unknown fields, so an older ATOM falls back to tokenizing/metricsmadviseoralways; the DCP prefill needsPREFILL_CPU_TIER=lmcache. Decode 0.94 / 2048, one QP + 16 send workers, Rust routerPREFILL_CPU_TIER=lmcacheon such hosts (README gotcha 14)5. Validation
Unit tests and checks:
infera/projection/core/projection/module_profilers/moe_mlp.py(unformatted since Projection accuracy: busiest-rank MoE load, measured decode sharding, sparse KV dtype #205), and the test jobs wait on lint. Run locally on the PR head with CI's commands:pytest -m "not slow and not integration"2,791 passed, 148 skipped;cargo fmt --check,cargo clippy --all-targets -D warningsandcargo testpass; pre-commit passes on every file this PR touches; shellcheck on the kit is clean.module_custom_all_reducerebuilds.check_dcp_merge.pypasses 24/24 on 4 and on 8 MI300X GPUs, and fails with the tolerance set to 0.41bd647abuilds on four different 8 × MI355X machines, and the results below come from it (96a3af86after it changes only kit scripts,check_dcp_merge.py, docs and comments).Correctness (real MTP acceptance):
Performance: AgentX with the InferenceX CI client, a cold start, warm-up and a 3600 s measured window per point, and forced MTP acceptance (K3, AL 2.99). GLM-5.2-MXFP4 on one 8 × MI355X node. Throughput is per GPU over all 8 GPUs and counts cache-hit input tokens. Every point had 0 errors and no JIT compile inside the measured window.
The new defaults (Store, Rust router; the four points split over three machines), against InferenceX PR #4045's 1P1D results (from that PR's own CI run, computed the same way from its published AgentX JSON; PR #4045 ran on other machines):
To reproduce a point (cold start;
agentx.shruns InferenceX918524ff's client, not the CI client used above):CONC=96 MTP_AL=2.99 SESSION_AFFINITY=off ROUTER_EXTRA_ARGS="--breaker-failure-threshold 0" PREFILL_EXTRA_ARGS="--spec-decode-acceptance-length 2.99" bash engine/up.sh && bash engine/agentx/agentx.sh && bash engine/down.sh.6. Known limitations / follow-ups
PREFILL_PP=1) cannot use the Store, since ATOM's Store offload refuses DCP, so it runs with LMCache. It was validated with LMCache and the previous decode settings and has not been re-run with the new decode settings.x-session-id/x-correlation-idto the engines (the Python router does).HSA_STATUS_ERROR_OUT_OF_RESOURCESwhile it computed the last 1,390 tokens of a 568K-token request itself. The rerun on another machine went through a 615K-token request of the same kind without errors; cause unknown.7. Merging
Let CI run once main's lint passes again (the
moe_mlp.pyformatting fix goes to main separately). The 17 commits are self-contained and all carrySigned-off-by, so the history can be kept (merge or rebase). Before merging, fix the message of14fb5592: it cites the pre-rebase hashesf7e46219and91910e57, which are now949a8f5dand8dd1f8b7. If the PR is squashed instead, the squashed message needsSigned-off-bytoo.🤖 Generated with Claude Code