Skip to content

[HLSL] Add thread-scope MatVec semantics coverage - #8879

Open
Jack Elliott (JoeCitizen) wants to merge 1 commit into
microsoft:mainfrom
JoeCitizen:linalg-hlk-thread-semantics
Open

[HLSL] Add thread-scope MatVec semantics coverage#8879
Jack Elliott (JoeCitizen) wants to merge 1 commit into
microsoft:mainfrom
JoeCitizen:linalg-hlk-thread-semantics

Conversation

@JoeCitizen

Copy link
Copy Markdown
Collaborator

Thread-scope matrix operations are per-thread, so every lane must load its own matrix and multiply against its own vector, but the existing thread-scope cases give every thread identical data and cannot distinguish that from an implementation that loads one matrix and broadcasts it across the wave. These two cases give each of eight threads a distinct 4x8 matrix in its own aligned slot, checked against a closed-form CPU oracle, and the second runs the same data under non-uniform control flow with each arm using a different vector. It branches on lane index rather than thread index, because thread-to-wave distribution is implementation defined and only lane parity makes the arms diverge inside a wave; each thread records a pre-branch witness so a run where that never happened is reported inconclusive rather than passed or failed.

Assisted-by: GitHub Copilot

Thread-scope matrix operations are per-thread, so every lane must load its own
matrix and multiply against its own vector. The existing thread-scope cases
give every thread identical data, so an implementation that loads one matrix
and broadcasts it to the whole wave passes them. Neither case here can pass
that way.

MatVecMul_Thread_4x8_F16_PerThread gives each of 8 threads a distinct 4x8
matrix in its own 128-byte aligned slot, with element (t,m,c) = Z + c where
Z = 4t + m + 1. Vector A is {1..8}, so the expected row result is the closed
form 36*Z + 168 over sum(c+1) = 36 and sum(c*(c+1)) = 168.

MatVecMul_Thread_4x8_F16_Divergent runs the same data under non-uniform
control flow, with each arm multiplying by a different vector. Vector B is all
-1, giving -(8*Z + 28) over sum(c) = 28. The two arms are therefore disjoint by
sign, so no arm-A result can be confused with an arm-B result anywhere in the
output. An earlier revision used all +1, which collided in two places
(A(Z=1) = B(Z=22) = 204 and A(Z=3) = B(Z=31) = 276), so a swap of those slots
would have been invisible. Because the arm is chosen by lane parity, any thread
may legally land on either arm, so the portable bounds are A in [204, 1320] and
B in [-284, -36]. The largest single product is 312 and every partial sum is
bounded by its own final magnitude, so all inputs, products and results are
exactly representable in F16.

The divergent case branches on WaveGetLaneIndex() rather than
SV_GroupThreadID. Thread parity only guarantees that different threads take
different arms; the distribution of threads to waves is implementation defined
(hlsl-specs 0048-group-wave-index.md), so an implementation that segregates
parities into separate waves would make both arms wave-uniform and the case
would prove nothing while still passing. Lane parity guarantees divergence
inside the wave.

Because that guarantee still depends on the wave containing more than one
active thread, each thread records a witness word before the branch holding
its predicate bit and a WaveActiveAnyTrue vote in both directions. The host
checks that at least one thread reports a wave that executed both arms.

If none does, the case reports Skipped rather than Failed. Implementations may
launch additional waves, those waves may contain inactive lanes, and the
distribution of threads among them is implementation defined, so a wave holding
only same-parity lanes is legal. Failing there would reject a conformant
implementation for a precondition this test cannot itself guarantee. The
per-thread results are still verified first and a wrong value still fails, so
the downgrade applies only when nothing else is wrong.

Each arm also writes a branch-local marker. The host requires the marker to
agree with the pre-branch witness, and validates results against the witness
rather than the marker. Validating against the marker would be circular: an
implementation that executed the wrong arm would write that arm's marker, and
the host would then check it against the oracle for the arm that actually ran
instead of the one the predicate selected.

Assisted-by: GitHub Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 83725f5d-8e98-4c1d-91ee-ad47629e007b

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The divergent test must skip devices without WaveOps support before shader compilation or execution.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds thread-scoped matrix-vector multiplication tests using per-thread data and divergent control flow.

Changes:

  • Adds distinct per-thread F16 matrices with CPU-oracle validation.
  • Adds lane-parity divergence coverage with execution witnesses.
  • Requires a WaveOps capability guard for the divergent test.
  • Includes two style nits requesting explicit return types instead of auto.
File summaries
File Description Findings
tools/clang/unittests/HLSLExec/LinAlgTests.cpp Adds shaders, fixtures, verification, and test cases. 1 moderate WaveOps guard issue (2 votes); 2 explicit-type nits (1 vote each).
Review details

Suppressed comments (2)

tools/clang/unittests/HLSLExec/LinAlgTests.cpp:7347

  • Use the explicit return type here per the project's almost-never-auto style. createComputeOp returns the straightforward std::unique_ptr<st::ShaderOp> type (HlslExecTestUtils.h:611-614).
  auto Op = createComputeOp(Shader, "cs_6_10", "SRV(t0), SRV(t1), UAV(u2)",
                            Args.c_str());

tools/clang/unittests/HLSLExec/LinAlgTests.cpp:7355

  • Use the explicit return type here per the project's almost-never-auto style. runShaderOp returns std::shared_ptr<st::ShaderOpTestResult> (HlslExecTestUtils.h:640-645), which remains clear enough to spell out.
  auto Result = runShaderOp(
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tools/clang/unittests/HLSLExec/LinAlgTests.cpp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

2 participants