Repository navigation
Cleanup trace splitter code - #1612
Conversation
ea6d689 to
6db0f28
Compare
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
|
/review |
|
Automated review started for head b3cc7a7. Progress: https://github.com/AMD-AGI/Hyperloom/actions/runs/37815170112 |
There was a problem hiding this comment.
PR #1612 -- Cleanup trace splitter code
What it does: Replaces the blanket --skip-split gate that kept scriptable (non-LLM-inference) frameworks out of trace splitting with per-kind flags — --steady-state-mode generic for scriptable, --split-llm-inference for serving — extracting the split pipeline into testable functions, adding a generic chunk family, and deleting the selfcert split-forecast subsystem and the Hyperloom-side pretrim.
Blocking issues: 3
- [S4] Deleting the structural GPU-events gate lets a zero-GPU-event chunk reach analysis [verified]
Problem:_check_selected_chunk_has_gpu_eventswas deleted on the claim that the splitter cannot produce an empty chunk, but the only surviving gate,_check_selected_chunk_has_gpu_events_quality, returnsNonewhen the busy ratio is unmeasurable — which is exactly thenum_gpu_events=0case its helper_busy_ratiopunts with "the caller defers to the N25 structural gate", a gate this PR removed. The splitter's coverage gate grades whole-trace root attribution, not per-chunk content. src/hyperloom/agents/kernel/tools/tracelens_analysis.py:4597
Impact: A chunk with zero GPU events (TP=1 + CUDA-graph mixed windows where rocprofiler-sdk emits no kernel events for the replay — the case the deleted gate's own tests pinned) is fed to TraceLens analysis, producing the misleading "Compute %=~0, Idle %=~100" Executive Summary the gate existed to refuse.
Action: Keep the structural zero check — either restore the gate or make_validate_selected_chunktreatnum_gpu_events==0as blocking instead of deferring to a deleted callee. - [C1] The chunk-failure warnings that drive the roofline auto-retry are no longer emitted [verified]
Problem:_collect_split_chunksand_validate_selected_chunkraiseRuntimeErrorwithout appending the structured warning dicts; base appendedsteady_state_chunk_missing/steady_state_chunk_low_quality(withavailable_modes/non_empty_modes) totrace_health_warningsbefore raising, and_extract_steady_state_retry_modein the roofline executor reads only those dicts — it never parses the error string.steady_state_chunk_emptyalso still sits in_AUTO_RETRY_WARNING_CODESwith no producer left. src/hyperloom/agents/kernel/tools/tracelens_analysis.py:4583
Impact: Every missing-mode, empty-chunk and low-quality-chunk failure becomes a hard roofline failure; the N26 self-healing re-issue on the same trace is dead code, and the executor's generictracelens_analysis_failedwarning does not carry a retry mode.
Action: Emit the structured warnings (appended before the raise, or returned by_run_trace_splitand extended intotrace_health_warningsbymain()) so the existing retry consumer keeps working. - [X2] The description states a change the diff does not contain, and an upstream handoff the pin does not carry [verified]
Problem: Item 6 claims_PHASE_SUFFIX_REand its call site were removed; the symbol, its comment and the quoted example exist nowhere in the diff or the base tree. Item 5 claims pretrim warmup removal is "now handled inside the TraceLens splitter itself" (TraceLens#1080), but the pinnedTRACELENS_REFb79f906 (2026-09-29) predates that merge (2026-10-07), and this PR does not move the pin. src/hyperloom/agents/kernel/scripts/install.sh:160
Impact: At the current pin nobody trims the profiler-start rank-barrier transient — the reference capture's 15.78 s barrier step lands in the steady-state window and every downstream percentage is computed against that inflated denominator, the exact regression the deleted pretrim documented.
Action: MoveTRACELENS_REFto a commit containing TraceLens#1080 in this PR (with the compatibility doc, per the repo's four-places rule), remove the fabricated item 6, and state the pretrim behavioural change as a dependency on the new pin.
Checked: tracelens_analysis.py split pipeline (_build_split_cmd/_collect_split_chunks/_validate_selected_chunk/_run_trace_split), trace_analyze.py _build_trace_analyze_cmd, roofline.py auto-retry (_AUTO_RETRY_WARNING_CODES/_extract_steady_state_retry_mode), trace_selfcert.py verdict/certify_trace_dir, profile.py _steady_state_forecast, roofline_event.py, framework_registry.py, pinned TraceLens splitter (root_detection.py coverage gate, steady_state_window.py, trace_extraction.py, startup_transient.py absence) | Ran: pytest test_trace_splitting.py (10 pass), test_n25_steady_state_mode.py + test_trace_selfcert_observability.py + test_kernel_request_handlers_units.py (272 pass), test_profile_and_kernel_handlers.py (176 pass + 1 failure reproduced on base = environment), test_tracelens_csv.py (220 pass + 1 failure reproduced on base = environment), test_n36_chunk_quality_gate.py (7 pass), test_roofline_executor.py + test_roofline_executor_units.py (42 pass), test_sbd_v6_roofline_timeline.py (35 pass); direct reproduction of the empty-chunk acceptance on head vs steady_state_chunk_empty on base; full tla.main() drives of the missing-chunk and low-quality cases on head and base; _build_trace_analyze_cmd duplicate-flag and env-override runs; ruff on changed files | Base: f2b32cd | Head: b3cc7a7
|
S4 has been addressed. Created a "busy_ratio" metric in TraceLens. check_chunk_quality looks at this metric to handle both the no gpu work check and the little gpu work check. |
|
@chaojhou addressed the comments |
xiaofei-zheng
left a comment
There was a problem hiding this comment.
PR #1612 -- Cleanup trace splitter code
What it does: trace splitting was gated off for non-LLM-inference workloads (--skip-split) and the repo
carried its own pretrim and split-forecast reimplementations of logic TraceLens now owns. This PR routes
every framework through the TraceLens splitter -- serving frameworks get --split-llm-inference plus the
CONC/OSL/R hints, scriptable frameworks get a new --steady-state-mode generic that selects the splitter's
single steady_state_* chunk -- extracts the splitting block of main() into four testable functions,
merges the two chunk gates into _check_chunk_quality reading the splitter's new busy_ratio column, and
deletes pretrim_startup_transient, forecast_split and idle_gate along with the certificate fields they
fed.
Blocking issues: 4
-
[S4] The chunk-quality gate cannot tell a missing
busy_ratiocolumn from a 0% busy chunk, so against
any TraceLens older than the new pin it stops blocking entirely. [verified]
Problem:_read_busy_ratioreturns0.0both when the column is absent and when the chunk really is 0%
busy. With the column absent, every row reads 0.0, so no alternate can clear
oth_ratio >= thresholdandalternatesstays empty;if not alternates and sel_events > 0: return None
then passes every chunk that has any GPU events.gpu_busy_duration, which the two deleted gates read and
which older splitters do emit, is no longer read by any production line (only the docstring at
tracelens_analysis.py:375). src/hyperloom/orchestrator/trace_analysis/tracelens_analysis.py:429
Impact:_TRACELENS_REF_DEFAULTis a default andTRACELENS_ROOT/--tracelens-rootlet an operator
point at another checkout, which the install script has explicit machinery for. On such a pod a
structurally empty or 1%-busy chunk is now fed to TraceLens and produces the misleading high-idle
Executive Summary the gate exists to prevent, with no warning and no log line. The description (section 8)
says the check "degrades to busy_ratio=0.0", which reads as degrading strict; it degrades to off.
Action: detect the absent column explicitly -- fall back togpu_busy_duration / gpu_durationwhen
busy_ratiois not in the row, or emit a warning that the gate could not run -- and correct section 8. -
[C3]
StepPass.gpu_intervalsloses its only reader in this PR and is left accumulating one tuple per GPU
event on every self-certification scan. [verified]
Problem: the attribute is initialised at :255 and appended at :304 (cat == "kernel") and :308
(gpu_memcpy/gpu_memset). Its only reader was_idle_in_span, at merge base 788ab26
trace_selfcert.py:715, which this diff deletes; a grep of the head tree including tests returns three
hits, all writes. src/hyperloom/orchestrator/trace_analysis/trace_selfcert.py:255
Impact:certify_trace_dirholds a list proportional to the GPU event count of the capture -- production
rank traces reach millions of events -- for data nothing consumes. The PR is titled a cleanup and removes
the forecast subsystem; this is the piece of it left behind.
Action: deletegpu_intervalsand its two append sites. -
[X3] A
#:doc-comment survives the constant it documented and now asserts a certificate property this PR
removed. [verified]
Problem: "The splitter's internal mode names differ from the value a caller sets in
INFERENCE_OPTIMIZER_STEADY_STATE_MODE. Recommendations are reported in the caller's vocabulary so the
certificate is directly actionable." documented_CONSUMER_MODE, which this PR deletes; the certificate no
longer carriesrecommended_steady_state_modeorrecommended_splitter_modeeither. The block now sits
directly abovedef _iso_now(), where#:means nothing.
src/hyperloom/orchestrator/trace_analysis/trace_selfcert.py:65
Impact: the next reader of this module is told the certificate reports an actionable mode recommendation
that no longer exists anywhere in the tree.
Action: delete the block; while there,:48still justifies the root patterns by what they "would
forecast", and the forecast is gone. -
[X2] The description does not match the head tree on two checkable numbers. [verified]
Problem: the body saystest_trace_splitting.pyhas "10 tests" and lists four under
TestBuildTraceAnalyzeCmd; the file has 11, with five in that class --
test_scriptable_payload_override_cannot_shadow_genericat :381 is unlisted. Thebaseline-raise
exception says_check_chunk_qualityis "cognitive 33 / 101 lines";scripts/code_metrics_baseline.txt
committed in this same PR recordsfunction-lines ... _check_chunk_quality 96.
src/hyperloom/orchestrator/trace_analysis/tests/test_trace_splitting.py:381
Impact: the baseline-raise exception is the text a reviewer reads instead of re-deriving the metric, and
it quotes a number its own artifact contradicts.
Action: update both counts in the description.
Checked: tracelens_analysis.py (_build_split_cmd / _collect_split_chunks / _validate_selected_chunk /
_run_trace_split / _check_chunk_quality / _is_derived_trace / argparse), trace_analyze.py
(build_trace_analyze_cmd, all four scriptable x bypass branches), trace_selfcert.py (StepPass.scan,
build_verdict, certify_trace_dir), roofline.py (AUTO_RETRY_WARNING_CODES,
extract_steady_state_retry_mode), profile.py, roofline_event.py, bypass_trace_analysis.py (argv surface),
test_trace_splitting.py / test_n25 / test_n36 / test_tracelens_csv, and TraceLens at 647cbf2
(split_trace/main.py, steady_state_window.py, trace_extraction.py) | Ran: head-tree and merge-base greps for
every removed identifier (steady_state_chunk_empty, pretrim*, forecast_split, viable_modes, recommended*,
gpu_intervals, 6489fbc), glob-exclusivity check for steady_state* vs mixed_steady_state*, gh api reads of
the pinned TraceLens tree; local pytest blocked by the module-level TraceLens import, CI green at head |
Base: 788ab26 | Head: c70e45b
|
@xiaofei-zheng addressed comments |
|
Re-reviewed at head a76f356. Two of the four are fixed ( 1. [S4] Still open:
|
CI E2E report — ❌ Failed
platform error |
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
|
@xiaofei-zheng
Regarding X2, reduced the function lengths to be within limits. |
xiaofei-zheng
left a comment
There was a problem hiding this comment.
Re-reviewed at head 42779ce against the new merge base a56028d. The three points from the last round are addressed: the busy_ratio fallback is in, the pretrim scope question is answered (the delay_iters argument holds — I checked _workload_envs._profile_capture_window), and the function lengths came down. The merge of main is clean: all four files main touched since the old base kept their changes.
Four blocking issues remain, two of them introduced by the fix in dc184e93.
1. [S1] The busy_ratio fallback lets a zero-GPU-work chunk through on exactly the configuration it was added for
src/hyperloom/orchestrator/trace_analysis/tracelens_analysis.py:424-451 and :470-472
_read_busy_ratio returns None when busy_ratio is absent and gpu_duration is not positive, and the caller then does if sel_ratio is None: return None — accept.
A structurally empty chunk is exactly that shape. In TraceLens trace_extraction.py:156, gpu_dur is only accumulated when a GPU event was seen (max(end_time) - min(start_time) if start_time else 0), so a chunk with num_gpu_events=0 carries gpu_duration=0. I ran the function as it stands at this head against the two rows:
row {num_gpu_events:0, gpu_duration:0, gpu_busy_duration:0, busy_ratio:0} -> 0.0 (new splitter)
row {num_gpu_events:0, gpu_duration:0, gpu_busy_duration:0} -> None (old splitter)
row {num_gpu_events:2790, gpu_duration:4538984, gpu_busy_duration:2723452} -> 0.600 (old splitter, healthy)
With the new splitter the busy_ratio=0 column makes the gate reach sel_events == 0 and refuse, which is right. With an older splitter the same chunk returns None and is accepted, and TraceLens analyses an empty window. Note this is a regression against the previous head too: at a76f356 the missing column read as 0.0, which refused correctly. The fallback works on healthy chunks and fails open on the one case the gate exists for.
Fix: check num_gpu_events (and gpu_busy_duration) before the sel_ratio is None and sel_ratio >= threshold early returns, so the structural refusal is unconditional the way _check_selected_chunk_has_gpu_events was.
2. [S1] The 10 ppt anti-thrash margin is now applied to structurally empty chunks, so a recoverable failure became a hard one
tracelens_analysis.py:479-491 with src/hyperloom/orchestrator/actions/executors/roofline.py:133-163
_CHUNK_QUALITY_ALTERNATE_MARGIN exists so a chunk that works but not well does not thrash against an equally mediocre sibling — its own comment says "Alternate must beat the requested mode by this margin to avoid thrashing". The deleted _check_selected_chunk_has_gpu_events did not use it: it listed as non_empty_modes every alternate with other_events > 0 and other_busy > 0.0. After the merge, the margin governs both cases.
Trigger: requested mixed chunk is structurally empty (num_gpu_events=0, busy_ratio=0.0), and the best alternate sits at busy_ratio=0.07. 0.07 >= 0.05 passes the threshold but 0.07 - 0.0 < 0.10 fails the margin, so alternates is empty, the warning carries non_empty_modes: [], _extract_steady_state_retry_mode finds no candidate, and roofline fails without retrying. On the merge base the same inputs retried into prefilldecode and the session recovered on the same captured trace. A 7% busy chunk is not a corner case here — the default threshold is 5% and the remediation text cites a 1% case.
When the selected chunk has no GPU work at all, every alternate carrying GPU work is strictly better and there is nothing to thrash against. Fix: when sel_events == 0, populate non_empty_modes from every alternate with GPU work and skip the margin.
3. [X2] _check_chunk_quality is now measurably worse than the description's waiver claims, and the baseline file no longer matches the code
The baseline-raise section argues: "into one function at cognitive 33 / 96 lines. This is a net reduction from both predecessors" (36/103 and 35/100).
At this head that is no longer true. I ran complexipy 8.0.1 locally on the file: _check_chunk_quality is cognitive 46, higher than both predecessors, not a reduction. scripts/code_metrics_baseline.txt:219 still records 33 and :789 still records 96; the gate report at 42779ce reads 46 and 100 against them. The gate also lists a new cyclomatic-complexity violation, 25 > 20, and grep cyclomatic-complexity.*_check_chunk_quality scripts/code_metrics_baseline.txt confirms the baseline has no entry for it — the base-branch predecessor it replaces was 21.
So the waiver now covers a new violation and two regressions the description does not mention, on an argument its own numbers contradict. _build_trace_analyze_cmd did come back to its baseline of 104 lines — that half of the last round's point is resolved.
Worth saying plainly: findings 1 and 2 are both consequences of putting a structural refusal and a comparative quality judgement in one function. Splitting them back apart would fix the complexity and the two bugs together.
4. [X2] Two statements in the description are false at this head
- §8: "against an older splitter the column is absent and the check degrades to busy_ratio=0.0". It degrades to
gpu_busy_duration / gpu_duration, and to skipping the gate when neither is available. Those are opposite operator-visible outcomes, and an operator reads this line to decide whether the pin bump is mandatory. - §4: "If there is no GPU work, the check refuses the chunk unconditionally". Falsified by finding 1. This is the one guarantee §4 offers to justify deleting
_check_selected_chunk_has_gpu_events.
Checked: _check_chunk_quality / _read_busy_ratio / _collect_split_chunks / _validate_selected_chunk / _run_trace_split, _build_trace_analyze_cmd across all four (scriptable, is_bypass) combinations against both receivers' argparse, roofline.py _AUTO_RETRY_WARNING_CODES / _extract_steady_state_retry_mode / the one-retry bound, trace_selfcert.py after the deletion (no dangling state; build_verdict's keyword-only signature is legal), _workload_envs._profile_capture_window for the warm-up argument, and TraceLens at 647cbf2 (trace_extraction.py, split_trace/main.py, startup_transient.py) | Ran: _read_busy_ratio extracted verbatim from this head against four concrete execution_details.csv rows; complexipy 8.0.1 on tracelens_analysis.py; greps of scripts/code_metrics_baseline.txt; a merge-audit of the four files main changed since the old base | Base: a56028d | Head: 42779ce
… by format (#1826 follow-up) (#1862) * fix(gemm-tune): read current aiter's fused-MoE dispatch lines with one parser aiter e7d2453 prints the activation field of a fused-MoE dispatch tuple as a bare enum repr (<ActivationType.Silu: 0>). Hyperloom's own regex required it quoted, so on current aiter it extracted no key: no runtime-observed MoE CSV reached Forge, and fmoe_ck fell back to tuning the single most-missed key. Hyperloom now takes its dispatch keys and the untuned CSV rows from Forge's evidence parser, which already read the repr. That parser in turn learns the dispatch wording that names its kernels, which it skipped. gemm_shape_coverage splits tuples with the same function, so its act_type is "Silu" rather than "Silu: 0>" and tuned rows match the dispatches they serve. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(gemm-tune): route the MoE lane on the format the serving log shows it ran A checkpoint's declared expert format is what it stores, not necessarily what the kernels run. When the caller states no runtime precision (empty, auto, or only the checkpoint's toolkit name such as quark) and leaves the quant type to the router, the MoE tuners now route on the weight dtype and QuantType of the most-missed fused-MoE dispatch in --kernel-signature-log. The formats nested in the checkpoint still decide when the log has no such line yet (first run, no log, no MoE dispatch), and always decide the dense lane, which that line does not describe. A stated precision is unchanged and holds for both lanes. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(session): label a nested-format checkpoint with the weight format it stores Quark and compressed-tensors put their own name in quant_method and the weight format one level down, so a Quark MXFP4 checkpoint labelled its session "quark": the GEMM tuning prompt, the session manifest, the exported PRECISION, the diffusion peak lookup and the workload fingerprint all carried a toolkit name where a format belongs. summarize_model_config now reads the nested declaration, so the session is labelled fp4 (fp8 for a compressed-tensors FP8 checkpoint). The format vocabulary moves from forge's model analyzer into hyperloom.common.checkpoint_quant, so the session label and forge's lane routing read one table. Forge routes as before. A session precision that is only the format the checkpoint nests is handed to forge as "auto", which forge treats as it treated "quark": each lane routes on what the serving log and the checkpoint show it runs, instead of one label holding for MXFP4 experts and fp8 attention alike, and a compressed-tensors fp8 checkpoint is not pinned to per_token. * fix(gemm-tune): count a logged MoE QuantType only when a MoE tuner routes on it The logged weight dtype only counted when the MoE tuners had an arm for it, but the QuantType beside it was taken whatever it said: a member such as per_128x128 normalised to itself, fell through to the catch-all arm and skipped fmoe_ck for good, where the checkpoint's declared expert format used to run it. The QuantType now follows the dtype's rule, so a member no MoE tuner routes on leaves the declared format to decide. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…on environment A node loss during an integrate left pending_integrate at phase "ready" with the operator's stash recorded in a tree that lived in the dead container. On resume in a new container the task carried the operator's stopped confirmation with an empty result, so recovery held the sentinel as an unknown outcome, and a rollback would have refused anyway because the stash is absent from the new checkout: every resume stopped with patch_recovery_incomplete. - An integrate attempt now records an identifier in the per-worktree git directory of the tree it mutates (tree_instance). On resume, a different identifier at the same path means another checkout: the tree-side obligations (reset the candidate out, hand the stash back) went with the old tree, in every phase short of restored or accepted. The sentinel is marked restored with tree_replaced, artifact targets outside the tree are still restored from their preimages, and the stash that was not restored is reported. A matching identifier keeps today's behaviour, so a stash dropped or pruned from the same tree still refuses. - A task whose last outcome is an operator confirmation that it stopped, with no kept result on the bus and no stack row, now proceeds to rollback instead of being held as an unknown outcome. An accepted window is still held. - A sentinel written before identifiers existed is judged replaced only when the task was confirmed stopped, the recorded stash object is not in the tree's object store at all, and nothing of the candidate is in the tree: every patch still applies forward cleanly (an applied candidate, or one edited over by hand, keeps the refusal) and every artifact target inside the tree holds its recorded preimage. Co-Authored-By: Claude <noreply@anthropic.com>
Resume settled a crashed integrate_patch window from its task row only when the window carried a patch or an artifact. A candidate that changed only environment variables or server arguments has neither, so a resume after its KEEP was recorded on the task row, but before it reached the bus, cleared the sentinel as stale and the accepted candidate never reached the stack. Every window now has its task row read. A confirmed KEEP in the accepted phase is replayed into the stack, the same as for a patch candidate; a window with nothing in the tree is otherwise cleared as before, since there is nothing to roll back or hold. Co-Authored-By: Claude <noreply@anthropic.com>
…e-run it on the same input (#1890) * fix(roofline): remember a deterministic analysis failure and do not re-run it on the same recipe A roofline that fails in a way the same launch configuration reproduces (trace_split_no_steady_state, the TraceLens analysis timing out, a profile that writes no trace) was re-armed by cycle_start, the gain watermark, the kernel-entry reprofile and CLOSE on the unchanged recipe, holding the benchmark/profile/server lanes for another hour to fail the same way. The roofline executor now records such a failure in SharedState.analysis_failures against the recipe the task profiled (working_recipe_generation, stamped on the task at enqueue), and internal_analysis_params -- the entry every re-arm path goes through -- skips the analysis while the recipe is unchanged, logging why and leaving a prelude_arm_dropped marker. A KEEP bumps the generation and re-arms it. Other failures stay transient and keep the existing streak/cap behaviour. Co-Authored-By: Claude <noreply@anthropic.com> * fix(roofline): do not treat a profile with no trace files as a recurring failure ProfileExecutor stamps every single-node run that left no trace with error_class=no_trace_files, including a server that failed to boot, so a single transient failure would have stopped every roofline re-arm until the next KEEP. Only a trace that cannot be split into steady-state chunks and an analysis that times out are remembered; a missing trace stays with the existing failure streak. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Zeng <lzeng@amd.com> Co-authored-by: Claude <noreply@anthropic.com>
|
Thanks Kyle — I pulled 490161a and re-read it. All three code findings are fixed, and fixed the way I'd have asked for. Confirming what I checked, then one correction of mine and two lines of description left. Fixed, verified:
Withdrawing one of mine: my §4 finding ("refuses unconditionally" is false) no longer holds. With the One clarification on scope, so we're not talking past each other: X2 is not an edge case that needs a pinned old TraceLens, or any runtime condition. It is the description disagreeing with the code at head, which is true for every reader on every configuration. Separately, the margin finding was not old-splitter-dependent either — an empty What's left is two lines of text, and I'd rather not defer them:
Neither needs code or CI. With those two edits I have no blocking issues left and no objection to this landing — I agree the remaining hardening (the |
xiaofei-zheng
left a comment
There was a problem hiding this comment.
LGTM. All findings from the earlier rounds are resolved at bc7958b.
I edited two factual lines in the description directly rather than send them back: §8 now describes the actual busy_ratio fallback (gpu_busy_duration / gpu_duration, gate skipped only when neither is available and the chunk has GPU events), and the baseline-raise waiver now carries the real figures, cognitive 23 / 88 lines, which is what makes the net-reduction argument hold against the 36 and 35 of the two predecessors. No other text was touched.
Checked: _check_chunk_quality and the three helpers lifted to module scope in 490161a — the sel_ratio = 0.0 fallback after the sel_events > 0 guard closes the accept path for an unreadable ratio on a chunk with no GPU events, and the sel_events == 0 branch in the alternate loop skips the anti-thrash margin so the roofline retry gets a target again; _build_trace_analyze_cmd across all four (scriptable, is_bypass) combinations against both receivers' argparse; _collect_split_chunks generic/LLM chunk separation; roofline.py _AUTO_RETRY_WARNING_CODES and _extract_steady_state_retry_mode with the one-retry bound; trace_selfcert.py after the deletion, with every removed certificate key's reader removed in the same diff; the merge of main, where all four files main had touched since the previous base kept their changes.
Ran: complexipy 8.0.1 on tracelens_analysis.py (_check_chunk_quality cognitive 23, down from 46); _read_busy_ratio extracted verbatim from head against old- and new-splitter execution_details.csv rows; greps of scripts/code_metrics_baseline.txt; TraceLens at 647cbf2 read for busy_ratio (trace_extraction.py), the generic/inference split paths (split_trace/main.py) and the startup-transient trim (startup_transient.py, confirmed an ancestor of the pin).
The ci-e2e/run failure at this head is infrastructure — "Pod was terminated because the allocation this Pod ran on ended" — and the same job succeeded on the two preceding heads. Worth a rerun before merge, not a code issue.
Two items I'd leave for follow-up, neither reachable from what the splitter can emit: _read_busy_ratio clamps a non-finite busy_ratio to 1.0 rather than rejecting it, and a row with num_gpu_events > 0 and gpu_busy_duration == 0 is accepted when no alternate clears the margin.
…back-config-env _build_trace_analyze_cmd conflicted with the trace splitter cleanup (#1612). Its behaviour is kept as main has it: no --skip-split, the splitter hints behind --split-llm-inference for a non-scriptable TraceLens run only, and the generic steady-state mode for a scriptable TraceLens run. Its inline four-flag loop is the same rule as _optional_flags and is folded into it; the two branches it gated with "scriptable" and "not scriptable and not is_bypass" are one if/elif.
Summary
This PR refactors HyperLoom's trace splitting pipeline to support generic (non-LLM-inference) workloads and removes dead code.
PR restriction exceptions
baseline-raise_check_chunk_qualityconsolidates_check_selected_chunk_has_gpu_events(cognitive 36, 103 lines) and_check_selected_chunk_has_gpu_events_quality(cognitive 35, 100 lines) into one function at cognitive 23 / 88 lines. This is a net reduction from both predecessors and shouldn't be treated as a regression. Only the 88-line entry needs the waiver; the cognitive and cyclomatic figures are within their limits.size-exceptionThis PR removes ~1000 lines of code added in #1292 as the logic covered by that PR has been migrated into TraceLens. Additionally, this PR is meant to accompany the TraceLens trace splitting refactor and thus contains many changes.
Changes
1. Remove
--skip-splitscriptable gate (trace_analyze.py)LLM inference frameworks are defined as
serving. Non-LLM-inference workloads are defined asscriptable. Theif scriptable: cmd += ["--skip-split"]conditional in_build_trace_analyze_cmdwas meant to prevent non-LLM-inference workloads from being split with the TraceLens splitter. This conditional has been removed so that all frameworks now go through the splitting pipeline.Scriptable frameworks now enter the TraceLens split pipeline with
--steady-state-mode genericinstead of bypassing splitting with --skip-split. Traces whose splitting fails will fail with trace_split_no_steady_state where they previously produced an unsplit report.2. Wire
--llm-inferencefor serving frameworks (trace_analyze.py,tracelens_analysis.py)--split-llm-inferenceis passed totracelens_analysis.py, which forwards it as--llm-inferenceto the TraceLens splitter. This enables shape-based phase classification (decode vs prefill) for traces that lack serving annotations. The LLM-specific splitter flags (--CONC,--OSL,--R) are also gated behind this flag, they are only added when--split-llm-inferenceis set.3. Add
genericsteady-state mode (trace_analyze.py,tracelens_analysis.py)Previously,
--steady-state-modewas only meaningful for LLM inference traces. Scriptable frameworks never reached this code because--skip-splitbypassed the entire splitting pipeline.With the gate removed, scriptable frameworks now need a steady-state mode that matches how the TraceLens splitter handles non-LLM traces. The splitter has two output modes:
mixed_steady_state_*,decode_only_steady_state_*,prefilldecode_steady_state_*— same as before.steady_state_*chunk using duration-based steady-state windowing. There are no prefill/decode phases to distinguish.The
mixed/decode_only/prefilldecodemodes map to the LLM chunk prefixes. A generic trace producessteady_state_*which doesn't match any of them. So we addgenericas a new--steady-state-modechoice. In_build_trace_analyze_cmd, the mode is now set based on framework kind:--steady-state-modeis set, so it defaults tomixed— same behavior as before.--steady-state-mode genericis explicitly set. This tells_collect_split_chunksto look forsteady_state_*files instead of the phase-specific prefixes._collect_split_chunksis updated to collect generic chunks and exclude them from the LLM-inference chunk sets.4. Consolidate
_check_selected_chunk_has_gpu_events()and_check_selected_chunk_has_gpu_events_quality(tracelens_analysis.py)_check_selected_chunk_has_gpu_events()and_check_selected_chunk_has_gpu_events_qualityhave been consolidated into and replaced by_check_chunk_quality. If there is little GPU work, it selects a better chunk if it exists. However, if there is no GPU work, the check refuses the chunk unconditionally._check_chunk_qualitysurfaces asteady_state_chunk_low_qualitywarning. Thesteady_state_chunk_emptywarning code is removed; both cases now surface as steady_state_chunk_low_quality.5. Remove pretrim logic (
tracelens_analysis.py)All pretrim functions and their invocation in
main()are deleted:pretrim_startup_transient()_step_annotation_spans()_host_step_starts()_step_phase()_phase_step_medians()_build_pretrim_no_steady_state_warning()_PRETRIM_OUTLIER_FACTOR,_PRETRIM_MIN_STEPS,_PRETRIM_MAX_DROPPED_STEPS,_PRETRIM_MIN_PHASE_STEPSPretrim warmup removal is now handled inside the TraceLens splitter itself: AMD-AGI/TraceLens#1080.
6. Extract splitting logic into functions (
tracelens_analysis.py)Much of the inline splitting logic which now depends on the framework type are extracted into three functions:
_build_split_cmd()— Assembles the TraceLens splitter CLI argv. Gates LLM-specific flags (--llm-inference,--CONC,--OSL,--R) behindargs.split_llm_inference._collect_split_chunks()— Globs the split directory for chunk files, mapssteady_state_modeto the right chunk family (LLM: mixed/decode_only/prefilldecode, generic: steady_state), returns the selected chunk and metadata._validate_selected_chunk()— Runs the low-quality busy-ratio gate on the selected chunk.7. Remove splitting forecast from self-certification probe (
trace_selfcert.py)The self-certification probe (
trace_selfcert.py) produces a diagnostic report about whether a trace is usable. It contained a "split forecast" subsystem that duplicated the TraceLens splitter's logic to predict what splitting would produce — without ever running the splitter. This forecast had zero runtime impact: the splitter always ran independently afterward regardless of what the forecast predicted. The forecast results (viable_modes,recommended_steady_state_mode,split_forecast,idle_gate) were written to aselfcert.jsoncertificate file and recorded in the roofline event recorder's metadata, but no code path read them to make decisions about splitting, mode selection, or any other runtime behavior.We verified this by searching all consumers of the certificate data:
profile.py:254(_steady_state_forecast) — extracts forecast fields into metadata. Doesn't do anything with the metadata though.profile.py:270(_build_trace_validate) — passes certificate through to the event record, no conditionalsroofline_event.py:420— recordsrecommended_steady_state_mode,viable_modes,steady_state_forecastinto event metadata, never reads them back8. Update TraceLens commit SHA
TraceLens pin bumped to 647cbf2, which adds the
busy_ratiocolumn toexecution_details.csv._check_chunk_qualityreads this column. Against an older splitter the column is absent and the check falls back togpu_busy_duration / gpu_duration. When neither is available, a chunk carrying GPU events is let through unchecked and a chunk with no GPU events is still refused, so the pin bump restores full gating rather than being required for correctness.9. Remove
step_roots_sufficientgateThe self-cert probe no longer blocks annotation-less traces. The
step_roots_sufficientgate inbuild_verdictassumed the splitter required pattern-matched step annotations, but this is no longer required, making thegate a false negative. Removed the parameter and its blocking branch; the splitter's own execution_details.csv is the ground truth for chunk quality.
Tests
New test file:
test_trace_splitting.py(11 tests)TestCollectSplitChunks(4 tests) — unit tests for_collect_split_chunks()intracelens_analysis.py:test_llm_mixed_mode: given mixed/decode/prefilldecode chunk files, selects the mixed chunk correctlytest_generic_mode: given asteady_state_*chunk file, selects it via thegenericmodetest_no_chunks_raises: empty split directory raisestrace_split_no_steady_statetest_missing_mode_raises: requested mode has no chunks raisessteady_state_chunk_missingTestSplitterE2E(2 tests) — end-to-end tests that call_run_trace_split()directly (the same functionmain()uses) on synthetic traces. This tests the full splitting chain: args →_build_split_cmd→ TraceLens splitter subprocess →_collect_split_chunks→_validate_selected_chunk. Verifies the pipeline runs without errors and produces the expected output files:test_vllm_trace_produces_three_chunks: synthetic vLLM trace with step annotations +split_llm_inference=True→ splitter producesmixed_steady_state_*chunks,_collect_split_chunksselects the mixed chunktest_xdit_trace_produces_generic_chunk: synthetic xDiT trace with ProfilerStep markers +steady_state_mode="generic"→ splitter produces asteady_state_*chunk,_collect_split_chunksselects it via generic modeTestBuildTraceAnalyzeCmd(5 tests) — unit tests for_build_trace_analyze_cmd()intrace_analyze.py, verifying the orchestrator wires the correct flags based on framework type:test_serving_gets_llm_inference: vLLM (serving) gets--split-llm-inferencein the command, no--skip-splittest_scriptable_gets_generic_mode: xDiT (scriptable) gets--steady-state-mode generic, no LLM flags, no--skip-splittest_serving_forwards_conc_osl_r: CONC/OSL/R from the payload are forwarded for serving frameworkstest_scriptable_no_conc_osl_r: CONC/OSL/R are absent for scriptable frameworks even if present in payload