协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
passnet-solve【开发与工程】END-TO-END playbook for solving one PassNet sample: analyze the computation graphs, decide the optimization strategy (what to fuse, what to replace, what to leave alone), drive the iteration loop, and maximize the sample score. This is the ENTRY skill — invoke it first for any PassNet optimization task; it tells you when to use passnet-pattern-fusion…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
You are optimizing one PassNet sample. A sample = several graph variants of the same
subgraph (different dtypes float32/float16/bfloat16 and input seeds/batch dirs). You write
compiler passes into pass_dir/; the evaluator pattern-matches them into each variant's FX
graph, checks correctness vs eager, and measures speedup.
Per graph variant, the rectified speedup is:
| outcome | value |
|---|---|
| no pass matched, or crash/runtime error | 0.1 |
| matched but wrong numerics (baseline tol) | ≈0.15 effective |
matched + correct, e2e speedup s | ≈ s (even if s < 1) |
Sample score = geometric mean across ALL variants (then a tolerance-weighted aggregate that
≈ the geomean). Read references/passbench-internals.md for the exact math, the harness
mechanics, anti-cheat rules, and evaluation modes.
Consequences — internalize these:
pass_dir.tax ≈ compiled_e2e − your_kernel_µs − remaining_eager_ops_µs) and reuse it in ceiling estimates. You only win if the aten
work you absorb into your kernel(s) exceeds the tax.Explicit task instructions override detection — if your prompt says which mode to use (or gives SVC/SAMPLE variables), use that mode; a service answering on the port may belong to another tenant/GPU on a shared box.
curl -sS --max-time 5 http://127.0.0.1:${PASSNET_API_PORT:-8968}/health
"mode": "sample_access_service" → service mode: all file I/O via
HTTP with ?sample_path=$SAMPLE; GPU eval via POST /evaluate. See
references/passbench-internals.md §"Evaluation modes" for the exact curl recipes.{"status":"ok"...} without that mode → API-server mode (same endpoints,
no sample_path param).entry.sh, graph_list.txt) → local mode: edit pass_dir/ directly; evaluate with
bash entry.sh (or pass_evaluator if available). Local mode always works from inside
a sample dir regardless of what services are running.In service mode, still author files in a local scratch dir first, run the pre-flight
checker on them, then POST them. The service is the source of truth: after upload, call
/files and confirm the expected JSON manifest and at least one pass file are visible
before /evaluate. Use bounded curl timeouts. In Codex managed sandboxes, try normal
localhost service curl first and continue normally if it succeeds. If it fails with
HTTP000, curl exit code 7, an empty body, or a connection-style error, retry once and
then use the approved escalation path if available. A sandboxed connection error does
not prove the service is down. Keep curl as its own command segment, capture response
bodies to files, and capture HTTP status separately so status text never corrupts a JSON
body.
Get graph_list.txt and every variant's model.py + weight_meta.py (locally or via
/problem). Note for each variant: ops and their exact textual call form, tensor
shapes, dtypes. Variants usually share the op structure; shapes may differ — check, it
decides whether shape literals may appear in patterns.
Run the analyzer from passnet-feedback (CPU-only works; GPU enables timings):
SCRIPTS=<repo>/.claude/skills/passnet-feedback/scripts
python3 $SCRIPTS/analyze_graph.py --sample-dir <dir> [--bench]
It prints, per variant, the dynamo graph with per-node matchability classification and
(with --bench, GPU) per-node eager timings. This tells you (a) which nodes a pattern can
legally bind to, (b) where the time actually goes — the bottleneck.
Work through this in order:
Heavy-op regime check — do this before labeling any dominant conv/matmul "leave in the
vendor lib." A vendor kernel is near-optimal only inside the shapes/parameters it tuned for
and for the general form of its op; you can beat it only when the concrete parameters put
the op OUTSIDE that regime AND equal a cheaper computation. The clearest checkable win to
actively look for: a convolution with stride == kernel_size AND kernel_size > 1 AND
padding == 0 (non-overlapping windows) — disjoint receptive fields make it exactly a dense
patch matmul while cuDNN runs a heavier general path, so a tl.dot patch-gather kernel can win
big; treat it as an ambitious anchor AFTER shipping the tail/floor. For ops in the vendor's
sweet spot — 1×1 conv (already a cuBLAS GEMM), overlapping conv (im2col would blow up memory),
depthwise/grouped, general dense matmul — the reliable default is leave-in-aten + fuse-tail;
rewriting them usually loses. These are strong priors, not absolute bans: the completed eval
is the arbiter, so if parameters give a concrete reason to expect a win you may try — but only
after a floor is banked and only keep it if the eval beats the floor. Don't conclude a
conv/matmul is "unmatchable" (they are C-bound and match fine); see passnet-orchestrate §2.2.
Partition the graph into maximal matchable regions: connected runs of matchable
nodes (see passnet-pattern-fusion for the matchability rules) where the region has
exactly ONE output consumed outside it. For normal callable patterns, kwargs-form
Python-level F.* calls (for example F.relu(x, inplace=False)) usually become region
boundaries because the pattern tracer normalizes them differently. If such a node blocks
a high-value single-output region, use passnet-pattern-fusion's manual FX GraphModule
pattern escape hatch and require real check_pattern.py proof; otherwise leave it in
eager and fuse around it. Do not absorb inplace=True nodes merely because manual FX can
match them; either reproduce the eager mutation exactly across repeated calls or leave the
in-place node outside and fuse the downstream side-effect-free region.
Estimate the win for each region (numbers from --bench, or the heuristics in
passnet-triton-opt §"performance model"):
gain ≈ (sum of eager µs of absorbed nodes) − (fixed tax ~50 µs amortized over regions) − (your kernel µs).
conv / matmul: cuDNN/cuBLAS are near-optimal only inside the regime they tuned for
and for the op's general form. Reliable default: leave the heavy op in aten and fuse its
elementwise tail (bias, norm, activation, residual add) into one kernel; re-implementing
the op competes with the library on its home turf and usually loses.stride == kernel_size along each spatial dim AND kernel_size > 1 AND padding == 0.
Then the receptive fields are disjoint, so the conv is exactly a matmul of flattened patches
by the flattened weight (patches @ weight.reshape(out_ch, -1).T, plus bias). cuDNN still
runs a general im2col/implicit-GEMM path with layout overhead it cannot skip, so a tl.dot
patch-gather kernel that also folds the flatten/transpose/tail into its epilogue can win by
a wide margin (the patch-embedding stem pattern). This is a real algorithm change, not a
reimplementation of the same work. Treat it as an ambitious region AFTER the floor.tl.dot competes head-to-head and rarely wins.conv1d/2d/3d, matmul, F.linear are C-bound
functions the pattern tracer binds to fine — mirror the exact positional call form and they
match (see passnet-pattern-fusion §3). "It's a big cuDNN op" is never a reason to conclude
the pattern can't match.
Discipline is unchanged: ship the safe tail/floor first, then attempt any rewrite as an
additional ambitious region and REVERT (Step 7) if a completed eval does not beat the floor.view/reshape/permute/transpose/unsqueeze) cost ~0 in eager;
absorbing them only pays when it lets you bridge two compute ops into one kernel, or
erase a .contiguous() materialization.Total-eager-time sanity check: if the variant's whole eager forward is tiny (< ~150 µs e2e, i.e. a couple of cheap ops) the fixed tax means the ceiling is < 1. Don't burn time chasing >1; ship the safest correct pass (floor pass) and move on — 0.85 is a fine score for such a sample. If a narrow or floor pass has a completed eval, matches everywhere, is correct, and is still slower than eager, classify it as overhead-bound after checking region availability; keep it as the downside cap, revert any lower-scoring wider attempt, and stop unless there is a clearly larger legal region that absorbs enough eager work per launch to plausibly beat the tax.
Skip-list: never include in a pattern: RNG ops (torch.rand, dropout with
training=True), data-dependent control flow, ops you cannot reproduce faithfully
at the dtype's baseline tolerance (see tolerance table in references). Dropout with
training=False IS safe (identity) when its call form is matchable.
Multi-variant check: a pattern containing shape literals (for example, a view with fixed dimensions) only matches variants with those exact literals. If shapes differ across variants, either avoid literal-bearing nodes in patterns, or write one pass per shape family — every variant must end with ≥1 matching pass.
Housekeeping when starting on a sample: rm -rf pass_dir/__pycache__ and remove any
leftover .py/.json from previous occupants — pass_dir is imported as a package and
stale files/pyc can shadow your modules or load as unexpected extra passes.
Before any ambitious work, write + verify + evaluate the simplest possible pass:
one matchable compute node (prefer batch_norm/layer_norm/linear-tail/gelu/
sigmoid-class single op; a straightforward Triton kernel you cannot get wrong).
Once it scores (anything ≥ ~0.7), the sample's downside is capped. Keep it in pass_dir
until something better replaces it; ALWAYS re-evaluate after replacing.
check_pattern.py, from passnet-feedback):
it verifies load → AST validation → match-per-variant → replacement wiring → a one-shot
numeric smoke test, in seconds. Only then run the real evaluation.parse_eval_log.py gives per-variant
status/speedups + estimated sample score + failure classification). Only a nonempty,
JSON-parseable evaluation response counts as completed; if the response is empty,
interrupted, or non-JSON, leave metrics unknown and fix service/upload/access state first.pass_dir state; if an "improvement" lowers the score, revert.
In service mode keep local copies of every uploaded version so you can restore.| graph looks like | strategy |
|---|---|
| chain of elementwise + norm ops (MLP tail, norm+act+dropout) | one fused elementwise/row kernel over the maximal matchable region |
| broadcast/layout producer + affine/activation/residual consumers writing a large output | priority target: one legal single-output fused memory-bound kernel; verify it does not force a returned view or metadata-only layout into a costly materialization |
| concat or multi-branch producer → inference normalization → affine/activation | priority target when containment gives one normalized/final output; leave in-place downstream activations outside, and only include kwargs-form Python functional activations when a manual FX pattern pre-flight proves the exact region matches |
linear → (gelu/relu/silu) [→ dropout] | fuse linear's tail if linear is big (keep cuBLAS), or tl.dot+epilogue if small; both: measure |
conv with stride == kernel_size, kernel_size > 1, padding == 0 (non-overlapping windows) | reduces to a patch matmul: gather disjoint patches + tl.dot against the flattened weight, and fuse the flatten/transpose/tail into the same kernel. Treat the conv as the region ANCHOR, not a wall. Ship a tail floor first |
conv → bn/act/add (1×1, depthwise/grouped, or overlapping conv) | leave the conv in aten; fuse the tail ops into one kernel. Rewriting the conv usually loses (vendor sweet spot); the non-overlapping stride==kernel_size, kernel>1 conv above is the case that IS worth rewriting |
attention-ish (matmul → softmax(method form) → matmul) | softmax row-kernel (method-form softmax matches!); large generic matmuls stay aten; consider fusing scale/mask into the softmax kernel |
cat/stack of tensors + elementwise or repeated producers | write the final output directly; compare one segmented kernel with a small number of grouped per-branch launches when branches have very different indexing/work |
| mean/sum/norm reductions + broadcast arithmetic (L2-norm, RMSNorm, sq-distance) | single row-reduction kernel producing the final output (verified ~1.6–2.5× on ms-scale tensors) |
expand/broadcast → binary op writing a LARGE output | strong target even alone: aten broadcast-elementwise kernels often run 1.5–2.5× off the bandwidth roofline; a shape-specialized Triton kernel + erased layout nodes wins big (verified 2.3×) |
mostly view/permute/contiguous + 1 compute op | replace the compute op only; or compute-op kernel that reads with source strides (absorbing a contiguous) |
| single huge op that is a generic dense matmul / 1×1 / depthwise / overlapping conv | floor pass on an adjacent/auxiliary node; accept ≈1.0; reimplementing usually loses. (A non-overlapping stride==kernel_size, kernel>1 conv is the case worth rewriting — see that row.) |
graph contains torch.rand / training-mode dropout | keep RNG nodes OUT of patterns; optimize around them |
torch.empty/zeros/ones/full[_like]/as_tensor allocators plus shape/stride/
dtype/device accessors — every other aten op on the (poisoned) inputs raises during the
anti-cheat warmup, failing the variant.getattr(torch, ...) laundering, no no_dispatch(), no
monkeypatching the harness). It's classified as hacking behavior; assume it will score 0
on the leaderboard even if it slips through locally — and it's usually slower anyway.replacement_func() must return a stable module-level function, and with
output_pass_replacement_func_limit: 1 all your passes must return the SAME function
object — the shared-dispatch architecture in passnet-pattern-fusion is mandatory the
moment you have ≥2 pass files.End with: final score (last eval), pass_matched, per-variant speedup summary, list of pass files, 1-paragraph strategy description, and (if below ceiling) what blocked further gains.