perf(rocke/conv): add the pointwise fast path dgrad was missing - #11928
Conversation
✅ All Checks Passed — Ready for Review
📖 Need help? See the Policy FAQ for details on every check and how to fix failures. |
|
🎉 All checks passed! This PR is ready for review. |
4d45946 to
d986356
Compare
d986356 to
25cd02d
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Two moderate driver correctness issues and additional review findings remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds a pointwise 1×1 dgrad fast path, vector-width clamping, benchmark controls, a single-config driver, and updated case-study documentation.
Changes:
- Adds mirrored Python/C++ dgrad fast paths.
- Clamps oversized vector widths.
- Adds LDS-layout and CSV benchmark options.
- Adds dgrad driver support and documentation updates.
File summaries
| File | Review findings |
|---|---|
dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/instances/common/conv_implicit_gemm_dgrad.py |
Nit (2 votes): add numeric regression coverage for ungrouped 1×1 dgrad, including a partial M tile. Nit (1 vote): add regression coverage for oversized vector_size_b. |
dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/examples/gfx950/conv_dgrad/run_one_dgrad.py |
Moderate (3 votes): resolve or reject --split-k -1 before packing and launching. Nit (1 vote): qualify the per-combo LDS-layout explanation. Moderate (1 vote): reject non-dgrad -F values. |
dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/examples/gfx950/conv_dgrad/dgrad_lds_layout_case_study.md |
Nit (3 votes): scope the single-config driver claim to dgrad or provide a wgrad driver. Nit (1 vote): remove or redact measured performance data prohibited by platform/AGENTS.md. |
dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/benchmark/benchmark_implicit_gemm_conv.py |
Nit (3 votes): qualify the auto LDS-layout help text per configuration or scope it to gfx950. Nit (1 vote): make the worker comment reflect pipeline- and tile-dependent selection. |
dnn-providers/hip-kernel-provider/rocke/platform/cpp/instances/common/conv_implicit_gemm_dgrad.cpp |
Reviewed matching C++ implementation for the dgrad fast path and vector-width clamping. |
Review details
Suppressed comments (5)
dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/benchmark/benchmark_implicit_gemm_conv.py:1653
- The same shape-only assertion is repeated in this worker comment, but
default_lds_k_outeralso keys onwarp_tile_nandpipeline. On gfx1250, a single auto sweep can therefore contain both K-outer and M-outer specs. Please qualify this comment to the gfx950 case study or state that the predicate is evaluated per configuration.
# be compared on identical configs -- under "auto" the predicate is
# constant across a sweep of one shape, so a sweep measures one layout
# and silently has no baseline to compare against.
dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/examples/gfx950/conv_dgrad/dgrad_lds_layout_case_study.md:183
platform/AGENTS.mdexplicitly prohibits committing software-achieved performance data, including benchmarks, latency/throughput, and profiling results. This new Finding 4 records measured stall/occupancy findings, instruction counts, and runtime outcomes throughout the section. Keep the qualitative methodology and algorithmic explanation, but remove or redact the measured results before merging.
## Finding 4: what an ATT trace says is expensive is not what is on the critical path
dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/examples/gfx950/conv_dgrad/run_one_dgrad.py:77
- This help repeats that the dispatch predicate has the same answer for every combo, but this script accepts arbitrary
--archvalues and the predicate is pipeline-dependent: on gfx1250memcan select K-outer while validwaveletselects M-outer. Please make the explanation per-combo or scope it to gfx950.
help="force the K-outer B tile. The dispatch policy deduces this via "
"DgradConvSpec.default_lds_k_outer, which answers the same way for "
"every combo of a given shape -- so forcing it is the only way to A/B "
"the two layouts.",
dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/examples/gfx950/conv_dgrad/run_one_dgrad.py:115
parse_miopen_cmdreturns the raw-Fvalue, but_fwdis discarded here. A command using-F 1,-F 4, or-F 0is therefore silently accepted even though this is a dgrad-only driver and the help documents-F 2; the script then reports a dgrad kernel for a command describing a different operation. Reject non-2 values (or make the direction an explicit driver option) before building.
p, _dt, _fwd = parse_miopen_cmd(cmd)
dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/instances/common/conv_implicit_gemm_dgrad.py:1463
- This fixes a builder failure for explicit oversized
vector_size_b, but no committed test covers the new behavior. The existing dgrad correctness suite has no vector-size parameterized case, and byte-identity hashes cannot exercise a previously unbuildable configuration; add a regression test that constructs an oversized cap, verifies the build succeeds with a narrowed width, and preserves existing valid-width emission.
load_vec_b = CoalescedTileLoader.choose_vec(
tile_rows=block_n,
tile_cols=block_k,
block_size=threads,
max_vec=min(_def_vec_b, spec.vector_size_b),
vector_axis="row",
- Files reviewed: 5/5 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
f80028b to
8b8824d
Compare
…ying it vector_size_* is a cap, not a demand -- wgrad documents it that way and passes vector_size_c through as max_store_vec. The dgrad K-outer branch clamped an explicit vector_size_b through choose_vec; the M-outer branch took it verbatim, so an over-wide width passed is_valid_dgrad_spec and then raised from the coalesced tile loader deep in the builder. Route the M-outer branch through choose_vec as the K-outer branch already does, in both engines. Emission-neutral: choose_vec's accepted set is a strict subset of vecs_per_thread's, and the tile_n divisibility rule the validator already enforces makes the axis condition free, so it yields exactly the requested width wherever the verbatim path built. Verified by hashing the lowered IR across the whole sweep grid before and after -- zero changes on configs that already built, zero regressions, and 428 previously unbuildable M-outer configs now build. Byte-identity green at llvm20 and llvm22; differential green; dgrad correctness suite passes. This does not make is_valid_dgrad_spec sound -- other builder-side raises remain, and load_vec_a is still taken verbatim in both layouts. Those need their own change, since the A-side clamp is not emission-neutral.
--lds-k-outer {auto,on,off} forces the dgrad LDS layout. Under auto the
deducer answers the same way for every combo of a given shape, so a sweep
measures one layout and silently has no baseline to compare against;
forcing it is what makes an A/B possible. auto stays the default, so
normal runs are unchanged.
--csv-top N replaces a hardcoded rocke_results[:5] that ignored --top, so
--csv wrote 5 rows out of thousands. Defaults to 5, preserving the old
behaviour.
…ejected fixes Both LDS-layout case studies end their capture command with "-- python3 <single-config driver>" and no such driver existed. Add it: one config, one dispatch, so --kernel-regex matches one thing. It does no numeric verification on purpose -- torch's HIP runtime and rocke's fight over the process HIP context and timings come out multiples wrong when a verify and a timing loop share a process. --print-name-only gets the kernel name without touching the GPU queue. Record Finding 4 in the dgrad study: an ATT capture classifies the kernel as latency-bound on global memory and attributes roughly three quarters of stall to two structures -- a non-pipelining tilde load phase and a dependent-scalar-load block search. Both were implemented in both engines, verified, measured and rejected: the ISA changed exactly as designed and runtime did not move. A large stall bucket means "this is where waves sit", not "this cost is removable". The consequence is the useful part: strided dgrad is bound by total memory time, so the scheduling family is a dead end and a real win must reduce bytes moved. Also document the two ATT traps (hit-weighted totals; inline_frames stacks live under "stacks" and are outermost-first), and correct two stale claims -- the sweep now has --lds-k-outer, and compv3/compv4 are rejected only on WMMA, not on gfx950.
Forward conv and wgrad both special-case pointwise convolution; dgrad never did. grep for is_pointwise hits conv_implicit_gemm.py and conv_implicit_gemm_wgrad.py in nine places each and the dgrad instance in none, in both engines. For Y=X=1, stride 1, pad 0, dilation 1, ungrouped, the tilde decomposition is the identity: h_tilde_slice == Ho, w_tilde_slice == Wo, y_dot_slice == x_dot_slice == 1 and gemm_k == kpg, so the dY offset reduces exactly to m_sub*K + k_sub and the W offset to k_sub*C + c_val. Instead dgrad emitted a runtime signed divide plus a tautological bounds predicate INSIDE the K-loop -- the descriptor closures run per element per K-tile from emit_load_phase, which sits inside the scf_for. Take the algebraic form directly on that shape class. The 1x1 kernel drops 1295 -> 991 instructions; v_mul_lo 54 -> 16, v_mul_hi 29 -> 16, v_cndmask 49 -> 16. This is worth real time because short-K pointwise dgrad is execution-bound, not latency-bound -- ATT wave state is EXEC ~55% / WAIT ~20%, the inverse of a long-K shape -- so deleting instructions converts to runtime. Two earlier attempts on latency-bound regimes changed the ISA exactly as designed and moved runtime by zero; this one is measured on hardware and does not. Non-pointwise emission is untouched: the 3x3 stride-1 and stride-2 hsacos are byte-identical with and without the change, so only pointwise dgrad moves. Numerically the fast path is exact, not approximate -- relative error against dgrad_reference is identical to the generic path on three 1x1 shapes and a 3x3 control. Byte-identity green at llvm20 and llvm22 (dgrad parity config 4 is pointwise, so the gate covers the new path); differential green; dgrad correctness suite passes; check_golden reports no drift. Follow-ups, deliberately not in this commit: - No representative-IR case is pointwise dgrad (all 11 are 3x3), so the new path has parity coverage but no golden. Worth adding one. - Grouped pointwise still takes the generic path. Extending it means folding k_out_group_base into the dY offset and using cpg as the W stride; every pointwise shape measured so far is ungrouped, so it would be untested code today. - make_dgrad_dx_descriptor composes unmerge_magic with a naive NHWC re-merge that is algebraically m*C + c, leaving dead magic-division in the non-strided epilogue for every shape, not just pointwise.
run_one_dgrad.py took --split-k -1 but never saw the degree it resolved to. The builder resolves auto onto a dataclasses.replace copy and hands back only a KernelDef, so the driver kept a spec at -1: sub-GEMM records packed with split_k=1 K-padding, needs_atomic False for a kernel that is in fact atomic (dX never re-zeroed between iterations), and gridDim.z going out as -1. Rejecting is the right answer rather than resolving it here -- the shipped dgrad candidate pins split_k=1 and declines the CK formula, so there is no auto dispatch for a single-config trace to reproduce. Fixed --split-k N is unaffected. While in here, three claims that were true only on gfx950 or only for dgrad: --lds-k-outer's help, its call-site comment and --kouter's help all said the deducer answers the same way for every combo of a shape, which stops holding on gfx1250 where wavelet builds; the case study said this driver serves the wgrad study too, when it builds DgradConvSpec and nothing else; and --miopen-cmd's help documented -F 4 as meaningful when the direction field is discarded outright. Also adds test_fp16_pointwise_1x1 to the dgrad correctness suite. The pointwise flat-offset branches only had emitter-vs-emitter parity, which a shared wrong offset sails straight through -- mutating the dY stride to K+1 now fails the suite. The docstring is explicit that the branch's validity predicate is *not* covered: on a flat offset the out-of-range lanes land past the end of the buffer and the descriptor clamp has already zeroed them, so deleting either clause leaves the test green no matter what dims you pick.
--lds-k-outer was removed in #11602/#11762 under "selection, not knobs"; re-adding it to the sweep driver contradicted convolution.md, the wgrad case study and lds-optimization-rocke.md, and let a sweep measure a kernel dispatch cannot select. The sweep driver now calls DgradConvSpec.default_lds_k_outer unconditionally, as wgrad does. run_one_dgrad.py gains --kouter auto (the new default), which asks the same predicate so a plain trace captures the kernel that ships; on/off remain for the in-process A/B the numeric tests use.
--csv-top was a bare slice bound, so 0 wrote a headers-only CSV and a negative value dropped that many worst-ranked rows -- both after a full sweep and both exiting 0. Reject < 1 before the sweep starts. The dgrad case study's K-outer selector collected eight tests while the prose promised six (-k LdsKOuter selects exactly the six), and its dispatch-policy command pointed at library/ as if it were under platform/ rather than beside it, so it failed at collection.
PR:11978 moved the conv kernels, tests and sweep driver out of platform/. run_one_dgrad.py still imported rocke.benchmark / rocke.instances.common and failed at startup; the case study's replay commands and two comment references pointed at the pre-move paths.
8df053f to
9938ac0
Compare
TheRock Submodule Bump ActivityNewest first
|
ISSUE ID: AICK-1763
Motivation
Forward conv and wgrad both have a fast path for 1×1 convolutions. dgrad never got one.
For 1×1 with stride 1 and no padding, the address math is trivial, the offsets are simple multiply-adds. But dgrad was still running the general-case code: a runtime integer divide plus a bounds check that can never fail, and both of them inside the K-loop, so the cost repeats on every iteration. 1×1 shapes are common in real backward-data workloads, so this was worth fixing.
Technical Details
Four commits, branched off users/aledudek/transpose_load_bwd_weight_data_gfx1250 @ f02fad7.
Test Plan
Test Result
Submission Checklist