Skip to content

perf(shader): guard packed f16 special-value repairs - #5057

Closed
MarcZX10 wants to merge 2 commits into
boykopovar:mainfrom
MarcZX10:codex/guard-packed-f16-rules-4042
Closed

MarcZX10 wants to merge 2 commits into
boykopovar:mainfrom
MarcZX10:codex/guard-packed-f16-rules-4042

Conversation

@MarcZX10

Copy link
Copy Markdown
Contributor

What

Closes #4042. The packed v_pk_add_f16, v_pk_mul_f16, and v_pk_fma_f16/v_pk_fmac_f16 translations currently emit the exact special-value repair sequence for every lane, including ordinary finite inputs. This moves the existing bit-selection rules behind a per-instruction guard: input/result NaNs and, where the native add/multiply path needs it, enabled overflow-to-infinity cases enter the unchanged repair path. Ordinary finite results use the existing fast result directly. The repair code itself is preserved.

The regression case forces every lane through the slow path and compares the output bit-for-bit with the existing f32 rule path. It also checks ordinary randomized inputs through the existing native-vs-emulated comparison.

This shares the packed-f16 translation function with #4072. This branch retains the current f32 FMA lowering; if #4072 is integrated first, its native-FMA result path will need the same guard before this change is considered complete. No native-GPU or Palworld performance result is claimed here; the #4042 reporter offered to run that A/B measurement.

Tested

Linux, Release, lavapipe (Mesa). Built agc_driver_packed_float16_native_tests, agc_driver_packed_alu_tests, agc_driver_float16_fma_rounding_tests, and agc_driver_fp16_overflow_tests.

ctest passed all four focused tests. python3 tools/check_conventions.py --base upstream/main and git diff --check passed. Palworld performance was not measured in this environment.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none; overlaps perf(shader): compute v_pk_fma_f16 and v_pk_fmac_f16 with OpFmaKHR #4072 in the packed-f16 translation path and is called out above
  • Unblocks: none
  • GPUs tested: lavapipe, Mesa; Linux (no hardware GPU available)
  • AI-assisted: yes

@queeeef

queeeef commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Sorry for the slow reply, busy day. Here is the Palworld A/B for this head.

Result: no measurable change in this scene. The three heaviest shaders take the same GPU time per call with and without it, and presents per 10 s stay within run-to-run variation.

Setup

  • Windows 11 Pro 26300, RTX 2080 Ti (NVIDIA 610.74), MinGW-w64 GCC 15.2.0, RelWithDebInfo.
  • Base: main 5d933db02 plus the open PRs Palworld (PPSA23126) needs and local changes. perf(shader): compute v_pk_fma_f16 and v_pk_fmac_f16 with OpFmaKHR #4072 is not in it, so packed f16 fma takes the existing lowering.
  • With: the same base plus this PR's two commits (b5f68bc9d, 68ee4015b), applied unchanged.
  • Tests: with the change, the 43 f16 and packed ALU tests pass on this GPU, agc_driver_packed_float16_native with its all-lanes case included.
  • Scene: title screen, alternating runs (base, with, base, ...), three each, 110 s per run. APS5_PROFILE_GPU=1, last four 10 s windows of each run. The window was in the background in all six runs. Per-call times are higher than in a foreground run (about 13 ms for the first shader), the same for both builds.

GPU time per call ([gputime] by program, ms per dispatch, mean of four windows):

Run Shader 1 (1876 code words, wave32) Shader 2 (5992 words, wave32) Shader 3 (1368 words, wave64) Presents / 10 s
Base 1 21.1 8.57 3.94 192
With 1 21.6 8.53 4.12 184
Base 2 23.0 9.17 4.35 183
With 2 22.3 9.00 4.21 189
Base 3 17.4 8.66 3.84 212
With 3 23.1 9.25 4.37 159

All three are 32x1x1 or 64x1x1 compute shaders dispatched once per frame. The packed ops are about 30% of shader 1's GCN instructions (45 v_pk_fma_f16, 51 v_pk_add_f16, 60 v_pk_mul_f16, 26 v_pk_min_f16, 19 v_pk_max_f16, 20 v_fma_mix_f32 of about 950).

Why it doesn't show here: what the special-value rules cost is small next to the rest of the lowering.

  • SPIR-V size: shader 1 recompiles to 207,832 SPIR-V words without this change and 242,434 with it, as the rules stay behind the guard.
  • What the shader is made of: of about 46,000 instructions, about 8,000 are OpBitwiseAnd, 3,600 OpSelect, 3,600 OpBitcast, 2,800 OpBitwiseOr and 2,000 OpBitFieldUExtract. There are 480 f32 OpFMul.
  • One v_pk_mul_f16: with the guard, it is the native f16vec2 multiply plus about 10 instructions for the check and a branch. Before, it was the multiply plus about 20 for the rules. The op_sel repacking of each operand (two extracts, two masks, a shift and an or) is around it either way, and here is computed twice per op.
  • One plain f32 multiply: its operands and result are wrapped by the denormal flush and NaN handling, about 25 instructions with a branch.

So in Palworld's heavy shaders the f32 denormal and NaN handling and the operand repacking weigh more than the f16 rules this PR guards. The guard itself works as described. I have no counter for how often lanes take the slow path in this scene.

AI-assisted (Claude Code)

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
#4072 core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp L150-153, L157, L191-193
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp L147
core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp L188
core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp L17
core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp L1453
core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp L367

@MarcZX10

Copy link
Copy Markdown
Contributor Author

Thanks for the controlled A/B and for recording the setup, workload and test results. I read this as no measurable improvement in the Palworld title-screen scene: the three heaviest shaders and presents per 10 s stayed within run-to-run variation. The correctness tests pass, but this run does not demonstrate a gameplay performance gain, so I will not cite it as one. It also found no regression in this measured scene.\n\nI will keep the PR’s claim limited to avoiding unnecessary packed-f16 special-value repairs in the guarded cases; broader performance benefit still needs a workload that exercises those cases often enough to measure.

@CnRJay CnRJay 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.

Going by queeeef's A/B above, the trade-off here is negative. Shader 1 grows from 207,832 to 242,434 SPIR-V words (+17%), and GPU time per dispatch is unchanged within noise on an RTX 2080 Ti. A larger module means longer vkCreate*Pipelines for every shader that uses packed f16, which turns into hitches on first use for everyone, while the run-time gain doesn't show.

This is the same pattern as #4322/#5400. Guarding the rare path with a branch lets the GPU skip a few ALU ops per lane, but it pays in module size and control flow. Driver compilers often flatten such short branches back into selects, so the GPU saving may not exist on the final ISA. The repair sequence is a handful of integer ops, which GPUs run at full rate, so it is rarely the bottleneck. queeeef's breakdown points the same way: the operand repacking and the f32 denormal/NaN wrapping dominate these shaders, not the f16 rules.

If this is pursued, I'd suggest:

  • measuring pipeline creation time for those three shaders, base vs. change, on NVIDIA and RADV, alongside the GPU time;
  • or a version that keeps the repair as straight-line selects, removes the duplicated op_sel repacking queeeef found (operands computed twice per op), and shrinks the module rather than growing it.

Without one of those, I don't think this should go in as perf(.

@MarcZX10

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I agree the current evidence does not demonstrate a runtime win: the RTX 2080 Ti A/B is within noise, while shader 1 grows from 207,832 to 242,434 SPIR-V words. The test pass establishes the guarded translation's correctness, not its performance benefit.

I also agree pipeline-creation time is the missing measurement before keeping a perf claim. This Linux environment has no exposed Vulkan GPU, so I can't add a comparable NVIDIA/RADV pipeline timing from here. I won't present the existing title-screen run as a gain; the review's code-size and branch concern remains unresolved on this head.

@CnRJay

CnRJay commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Thanks, that's a fair summary. One direction you can verify without a GPU: the duplicated op_sel repacking queeeef found (each operand's extract/mask/shift/or computed twice per op) can be removed without touching the repair rules. That shrinks the module rather than growing it, so its benefit shows directly in SPIR-V word counts and in agc_shader_replay recompile time on a CPU-only machine, and correctness is covered by the same packed-f16 tests. If that version lands first, the guard question can be revisited later with a pipeline-creation measurement from someone who has the hardware.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Closing: as you noted above, the measurements so far show no gain, and the change affects every packed-f16 shader. Reopen with before/after numbers from one title scene if a gain turns up.

@MarcZX10

Copy link
Copy Markdown
Contributor Author

I carried the reviewer suggestion about duplicate op_sel repacking as #5811, stacked on #4072. It caches the selected low/high operand bits within packedFloat16 and reuses them in the native result and existing repair path; the math and repairs are unchanged. The changed translation unit compiles and the PR’s eligibility, conventions, and intake checks pass. Build jobs were skipped, and I did not run the focused execution tests or claim a runtime win.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: evaluate the packed f16 special-value rules only on lanes that can need them

4 participants