Skip to content

Commit 25df2cb

Browse files
committed
Merge remote-tracking branch 'origin/main' into upgrade-nixl-1.5.0
# Conflicts: # requirements-dev.txt
2 parents a699c14 + b22283a commit 25df2cb

8,347 files changed

Lines changed: 49374 additions & 45082 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/skills/trtllm-moe-develop/SKILL.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ Main APIs / references:
470470

471471
- `communication/base.py`: `Communication`, `is_platform_supported`,
472472
`is_workload_feasible`, `supports_post_quant_dispatch`, `prepare_dispatch`,
473-
`dispatch`, `combine`.
473+
`dispatch`, `combine`, `checkpoint_blocked_reason`.
474474
- `communication/communication_factory.py`: strategy selection.
475475
- Existing strategies: `nvlink_one_sided.py`, `nvlink_two_sided.py`, `deep_ep.py`,
476476
`allgather_reducescatter.py`.
@@ -483,6 +483,9 @@ Checklist:
483483
before dispatch.
484484
- `dispatch()` and `combine()` maintain enough internal state for the pair to be
485485
correct.
486+
- `checkpoint_blocked_reason()` returns `None` by default, meaning the strategy
487+
can be checkpointed. Override it when a configuration blocks checkpointing;
488+
`PyExecutor.__init__` uses it to reject `sleep_config` before the engine serves.
486489
- EPLB statistics gathered by the communication strategy are fed back to the
487490
load balancer through the forward-execution path.
488491
- Add/update `test_moe_comm.py` or module-level tests when changing strategy
@@ -673,7 +676,7 @@ Prefer the unified MoE tests:
673676
- Communication changes: `pytest tests/unittest/_torch/moe/test_moe_comm.py -k '<strategy>'`.
674677
- Routing changes: `pytest tests/unittest/_torch/moe/test_moe_routing.py -k '<routing>'`.
675678
- Load balancer changes: `pytest tests/unittest/_torch/moe/test_moe_load_balancer.py -k '<case>'`.
676-
- Multi-GPU EP/all-to-all behavior: `pytest tests/unittest/_torch/moe/multi_gpu/test_moe_a2a.py -k '<case>'`.
679+
- Multi-GPU EP/all-to-all behavior: `pytest tests/unittest/_torch/moe/multi_gpu/test_nvlink_one_sided.py -k '<case>'`.
677680

678681
When GPU resources are required, use the TRT-LLM GPU allocation/test-runner
679682
skills first and record skipped tests with reasons.

‎.claude/skills/trtllm-moe-develop/references/moe-canonical-code-examples.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ Use these examples when wrapper forward policy grows complicated:
371371
should move into the scheduler.
372372
- `tests/unittest/_torch/moe/test_moe_module.py`
373373
- Module-level multi-GPU, chunking, routing, and EPLB cases.
374-
- `tests/unittest/_torch/moe/multi_gpu/test_moe_a2a.py`
374+
- `tests/unittest/_torch/moe/multi_gpu/test_nvlink_one_sided.py`
375375
- Multi-GPU all-to-all behavior when relevant.
376376

377377
Good uses:
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
const DAY = 24 * 60 * 60 * 1000;
5+
const WAITING = 'waiting for feedback';
6+
const STALE = 'stale';
7+
const BOT_LOGINS = new Set([
8+
'coderabbitai', 'coderabbitai[bot]', 'trtllm-agent',
9+
'svc-trtllm-gh-bot', 'tensorrt-cicd', 'github-actions[bot]',
10+
]);
11+
12+
function isBot(user) {
13+
const login = (user?.login || '').toLowerCase();
14+
return user?.type === 'Bot' || login.endsWith('[bot]') || BOT_LOGINS.has(login);
15+
}
16+
17+
// Timeline `committed` events carry git author/committer objects (name, email,
18+
// date) without a GitHub login, so bots are recognised by name or email.
19+
function isBotCommitter(person) {
20+
const name = (person?.name || '').toLowerCase();
21+
const email = (person?.email || '').toLowerCase();
22+
return name.endsWith('[bot]') || BOT_LOGINS.has(name) || email.endsWith('[bot]@users.noreply.github.com');
23+
}
24+
25+
function timestamp(value) {
26+
const time = Date.parse(value);
27+
if (!Number.isFinite(time)) throw new Error(`Missing or invalid activity timestamp: ${value}`);
28+
return time;
29+
}
30+
31+
// Only these timeline events are responses to a feedback request. Everything
32+
// else (labels, assignments, review requests, cross-references, milestones,
33+
// renames) is maintainer bookkeeping or unrelated traffic, regardless of actor.
34+
const ACTIVITY_EVENTS = new Set(['head_ref_force_pushed', 'reopened', 'ready_for_review']);
35+
36+
// Returns one of: skip, mark, answered (author responded before the PR went
37+
// stale: drop waiting for feedback), unstale (author responded: drop both labels),
38+
// lift (another human responded: drop only stale), baseline, close.
39+
function decide({issue, timeline, comments, reviews, reviewComments, head}, now) {
40+
const labels = issue.labels.map(label => label.name.toLowerCase());
41+
if (issue.state !== 'open' || !issue.pull_request || !labels.includes(WAITING)) return 'skip';
42+
const author = (issue.user?.login || '').toLowerCase();
43+
const isAuthor = user => author !== '' && (user?.login || '').toLowerCase() === author;
44+
let lastActivity = timestamp(issue.created_at);
45+
let authorActivity = lastActivity;
46+
let waitingAt = lastActivity;
47+
let waitingKnown = false;
48+
let waitingRemovedAt;
49+
let staleAt;
50+
const record = (time, byAuthor) => {
51+
lastActivity = Math.max(lastActivity, time);
52+
if (byAuthor) authorActivity = Math.max(authorActivity, time);
53+
};
54+
for (const event of timeline) {
55+
const label = event.label?.name?.toLowerCase();
56+
if (label === WAITING && event.event === 'labeled') { waitingAt = timestamp(event.created_at); waitingKnown = true; }
57+
if (label === WAITING && event.event === 'unlabeled') waitingRemovedAt = timestamp(event.created_at);
58+
if (label === STALE && event.event === 'labeled') staleAt = timestamp(event.created_at);
59+
if (label === STALE && event.event === 'unlabeled') staleAt = undefined;
60+
if (!ACTIVITY_EVENTS.has(event.event) || isBot(event.actor)) continue;
61+
record(timestamp(event.created_at), isAuthor(event.actor));
62+
}
63+
// Commits are counted as author activity: they come from the PR branch and
64+
// the git identity cannot be mapped to a login. Unknown identities must not
65+
// cause a PR with new code to be closed, so only known bots are excluded.
66+
for (const event of timeline.filter(event => event.event === 'committed')) {
67+
if (isBotCommitter(event.author) && isBotCommitter(event.committer)) continue;
68+
record(timestamp(event.committer?.date || event.created_at), true);
69+
}
70+
for (const item of [...comments, ...reviews, ...reviewComments]) {
71+
if (isBot(item.user) || item.state === 'PENDING') continue;
72+
record(timestamp(item.updated_at || item.submitted_at || item.created_at), isAuthor(item.user));
73+
}
74+
// A pushed commit can retain an old commit date, and an ordinary push leaves
75+
// no timeline event. Recorded heads catch these updates as well as rebases,
76+
// even when the committer is an automation. `assessment-head` is written by
77+
// the external PR-triage publisher (svc-trtllm-gh-bot) when it labels a PR;
78+
// `stale-head` and `stale-baseline` by this script. All record the full
79+
// 40-hex head SHA. Newest record first.
80+
const headRecords = comments.filter(comment =>
81+
['svc-trtllm-gh-bot', 'github-actions[bot]'].includes(comment.user?.login)
82+
).flatMap(comment => {
83+
const created = timestamp(comment.created_at);
84+
const body = comment.body || '';
85+
const initial = body.match(/<!-- (?:assessment|stale)-head:([0-9a-f]+) -->/);
86+
const recovery = body.match(/<!-- stale-baseline:(\d+) head:([0-9a-f]+) -->/);
87+
if (recovery) return [{head: recovery[2], created, staleAt: Number(recovery[1])}];
88+
if (initial) return [{head: initial[1], created}];
89+
return [];
90+
}).sort((a, b) => b.created - a.created);
91+
if (!labels.includes(STALE)) {
92+
// The label workflow clears the request on comments only; pushes and formal
93+
// reviews by the author are answers too, as is a head that moved since the
94+
// newest head recorded for this request. Legacy PRs without a label event
95+
// have no request time to compare against and keep the label.
96+
const requestHead = headRecords.find(record => record.created >= waitingAt)?.head;
97+
if (waitingKnown && authorActivity > waitingAt) return 'answered';
98+
if (waitingKnown && head && requestHead && head !== requestHead) return 'answered';
99+
return now - Math.max(lastActivity, waitingAt) >= 14 * DAY ? 'mark' : 'skip';
100+
}
101+
// Never infer a closure deadline from updated_at: bot writes also update it.
102+
if (staleAt === undefined) throw new Error('Stale label has no corresponding timeline event');
103+
// The label workflow removes only `waiting for feedback` when the author
104+
// replies and re-adds it on the next maintainer comment, leaving `stale` from
105+
// the previous cycle in place. A request withdrawn and re-issued after the
106+
// stale label starts a fresh cycle: drop `stale` and let the pre-stale path
107+
// mark again. Labels applied together, in any order, do not qualify.
108+
if (waitingRemovedAt !== undefined && waitingRemovedAt >= staleAt && waitingAt > waitingRemovedAt) return 'lift';
109+
const baseline = headRecords.find(record => record.staleAt === undefined
110+
? record.created <= staleAt : record.staleAt === staleAt && record.created >= staleAt);
111+
const recordedHead = baseline?.head;
112+
if (head && recordedHead && head !== recordedHead) return 'unstale';
113+
if (authorActivity > staleAt) return 'unstale';
114+
if (lastActivity > staleAt) return 'lift';
115+
// Legacy/manual stale labels may predate head tracking. Start a new grace
116+
// period rather than closing based on commit dates that do not record pushes.
117+
if (!recordedHead) return 'baseline';
118+
return now - Math.max(staleAt, baseline.created) >= 14 * DAY ? 'close' : 'skip';
119+
}
120+
121+
async function readSnapshot(github, repo, number) {
122+
const args = {...repo, issue_number: number, per_page: 100};
123+
const {data: issue} = await github.rest.issues.get(args);
124+
const timeline = await github.paginate(github.rest.issues.listEventsForTimeline, args);
125+
const comments = await github.paginate(github.rest.issues.listComments, args);
126+
const pullArgs = {...repo, pull_number: number, per_page: 100};
127+
const reviews = await github.paginate(github.rest.pulls.listReviews, pullArgs);
128+
const reviewComments = await github.paginate(github.rest.pulls.listReviewComments, pullArgs);
129+
const {data: pull} = await github.rest.pulls.get(pullArgs);
130+
return {issue, timeline, comments, reviews, reviewComments, head: pull.head.sha};
131+
}
132+
133+
async function run({github, context, core, now = Date.now(), dryRun = false}) {
134+
const repo = context.repo;
135+
const issues = await github.paginate(github.rest.issues.listForRepo, {
136+
...repo, state: 'open', labels: WAITING, per_page: 100,
137+
});
138+
for (const candidate of issues.filter(issue => issue.pull_request)) {
139+
const args = {...repo, issue_number: candidate.number};
140+
try {
141+
const before = await readSnapshot(github, repo, candidate.number);
142+
const action = decide(before, now);
143+
core.info(`#${candidate.number}: ${action}${dryRun ? ' (dry run, not applied)' : ''}`);
144+
if (action === 'skip' || dryRun) continue;
145+
// Re-read all activity before writing; skip changing PRs until the next run.
146+
const fresh = await readSnapshot(github, repo, candidate.number);
147+
if (JSON.stringify(before) !== JSON.stringify(fresh)) {
148+
core.info(`#${candidate.number}: changed while checking; skipped`);
149+
continue;
150+
}
151+
if (action === 'unstale') {
152+
await github.rest.issues.removeLabel({...args, name: STALE});
153+
await github.rest.issues.removeLabel({...args, name: WAITING});
154+
await github.rest.issues.addLabels({...args, labels: ['Investigating']});
155+
} else if (action === 'answered') {
156+
await github.rest.issues.removeLabel({...args, name: WAITING});
157+
await github.rest.issues.addLabels({...args, labels: ['Investigating']});
158+
} else if (action === 'lift') {
159+
// Another human responded; the team is still waiting on the author.
160+
await github.rest.issues.removeLabel({...args, name: STALE});
161+
} else if (action === 'mark') {
162+
await github.rest.issues.createComment({...args,
163+
body: `PR has not received a human update in over 14 days. Adding stale label.\n\n<!-- stale-head:${fresh.head} -->`});
164+
await github.rest.issues.addLabels({...args, labels: [STALE]});
165+
} else if (action === 'baseline') {
166+
const staleEvent = fresh.timeline.filter(event =>
167+
event.event === 'labeled' && event.label?.name?.toLowerCase() === STALE
168+
).at(-1);
169+
await github.rest.issues.createComment({...args,
170+
body: `We are restarting the feedback window to allow a full 14 days for an update before automatic closure.\n\n<!-- stale-baseline:${timestamp(staleEvent.created_at)} head:${fresh.head} -->`});
171+
} else if (action === 'close') {
172+
// Close before posting so a failed close cannot leave a misleading notice.
173+
await github.rest.pulls.update({...repo, pull_number: candidate.number, state: 'closed'});
174+
await github.rest.issues.createComment({...args,
175+
body: 'This PR was closed because it has been 14 days without human activity since it was marked as stale.'});
176+
}
177+
} catch (error) {
178+
core.setFailed(`#${candidate.number}: ${error.message}`);
179+
}
180+
}
181+
}
182+
183+
module.exports = {run, decide, isBot, isBotCommitter};

0 commit comments

Comments
 (0)