[llvm-calc-occupancy] Add an AMDGPU occupancy calculator tool (#208727)

Add a command-line utility that reports the occupancy (waves/EU) an
AMDGPU kernel would reach for a given workgroup size, VGPR/SGPR usage
and LDS. It reuses the backend's own occupancy math, so the numbers
match the compiler. Includes a lit test and a CommandGuide man page.

Assisted-by: Claude Opus

---------

Co-authored-by: mselehov <mselehov@amd.com>
This commit is contained in:
michaelselehov 2026-07-30 15:11:33 +02:00 committed by GitHub
parent 289a4568f7
commit b11722f1d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 627 additions and 0 deletions

View file

@ -15,6 +15,7 @@ llvm-addr2line
llvm-ar
llvm-as
llvm-bcanalyzer
llvm-calc-occupancy
llvm-cgdata
llvm-config
llvm-cov
@ -143,6 +144,7 @@ interested in.
* {doc}`llvm-profgen`
* {doc}`llvm-tli-checker`
* {doc}`llvm-offload-binary`
* {doc}`llvm-calc-occupancy`
## Remarks Tools

View file

@ -0,0 +1,109 @@
llvm-calc-occupancy - AMDGPU occupancy calculator
==================================================
.. program:: llvm-calc-occupancy
SYNOPSIS
--------
:program:`llvm-calc-occupancy` -mcpu=<gfxNNN> [*options*]
DESCRIPTION
-----------
:program:`llvm-calc-occupancy` reports the occupancy (waves per execution unit)
that an AMDGPU kernel would achieve for a given combination of workgroup size,
VGPR usage, SGPR usage and LDS allocation. It is a thin front-end over the same
occupancy math the AMDGPU backend uses (``GCNSubtarget``), so the numbers match
what the compiler computes for an equivalent kernel.
Any resource that is not specified is treated as unconstrained. When the
workgroup size is left unspecified (or given as a range), the occupancy is
reported as a range as well.
The tool only supports the AMDGPU target. New callers should use the
``amdgpu`` triple (for example ``amdgpu-amd-amdhsa``); the legacy ``amdgcn``
spelling is still accepted.
EXAMPLE
-------
.. code-block:: console
$ llvm-calc-occupancy -mcpu=gfx90a --wg-size=512 --vgprs=50 --sgprs=30
llvm-calc-occupancy - AMDGPU occupancy calculator
Target
Triple: amdgpu-amd-amdhsa
GPU (-mcpu): gfx90a
Wavefront size: 64
Max waves/EU: 8 (waves per SIMD, hardware limit)
EUs (SIMDs) per CU: 4
...
Per-constraint occupancy (waves/EU)
Workgroup + LDS: 8
VGPRs: 8
SGPRs: 8
Result
Occupancy: 8 waves/EU (32 waves/CU)
Limited by: workgroup size / LDS, VGPRs, SGPRs
Next step: already at the hardware maximum
OPTIONS
-------
.. option:: -mcpu=<gfxNNN>
Target GPU, for example ``gfx90a``. Required.
.. option:: -mtriple=<triple>
Target triple. Defaults to ``amdgpu-amd-amdhsa``. Must be an AMDGPU triple
(the ``amdgpu`` or legacy ``amdgcn`` arch).
.. option:: -mattr=<features>
Comma-separated list of subtarget features, for example
``+wavefrontsize32``.
.. option:: --wg-size=<N>, --flat-workgroup-size=<N>
Flat workgroup size. Accepts a single value ``N`` or a range ``MIN:MAX``.
When omitted, the full legal range is assumed and the occupancy is reported
as a range.
.. option:: --vgprs=<N>
Number of VGPRs used per lane. When omitted, VGPRs do not constrain the
occupancy.
.. option:: --sgprs=<N>
Number of SGPRs used per wave. When omitted, SGPRs do not constrain the
occupancy.
.. option:: --lds=<size>
LDS bytes allocated per workgroup. Accepts an optional ``k``/``kb`` or
``m``/``mb`` suffix (base 1024).
.. option:: --dynamic-vgpr-block-size=<N>
Dynamic VGPR block size. ``0`` (the default) disables dynamic VGPR mode.
.. option:: --limits
Print, for each occupancy level supported by the GPU, the maximum number of
VGPRs and SGPRs that still reaches that level.
.. option:: --help
Print a summary of command line options and exit.
EXIT STATUS
-----------
:program:`llvm-calc-occupancy` returns 0 on success and a non-zero exit code if
the arguments are invalid (for example a missing or non-AMDGPU target).

View file

@ -187,6 +187,11 @@ if (TARGET llvm-mt)
list(APPEND LLVM_TEST_DEPENDS llvm-mt)
endif ()
# Only built when the AMDGPU target is enabled.
if (TARGET llvm-calc-occupancy)
list(APPEND LLVM_TEST_DEPENDS llvm-calc-occupancy)
endif ()
if(LLVM_INCLUDE_EXAMPLES)
list(APPEND LLVM_TEST_DEPENDS
Kaleidoscope-Ch3

View file

@ -341,6 +341,7 @@ tools.extend(
ToolSubst("OrcV2CBindingsLazy", unresolved="ignore"),
ToolSubst("OrcV2CBindingsVeryLazy", unresolved="ignore"),
ToolSubst("dxil-dis", unresolved="ignore"),
ToolSubst("llvm-calc-occupancy", unresolved="ignore"),
]
)

View file

@ -0,0 +1,58 @@
## Basic checks for the AMDGPU occupancy calculator.
REQUIRES: amdgpu-registered-target
## VGPR-limited kernel on gfx1030 (wave32, 16 waves/EU HW limit).
RUN: llvm-calc-occupancy -mcpu=gfx1030 --vgprs=128 | FileCheck %s --check-prefix=VGPR
VGPR: GPU (-mcpu): gfx1030
VGPR: Max waves/EU: 16
VGPR: VGPRs: 8
VGPR: Occupancy: 8 waves/EU (32 waves/CU)
VGPR: Limited by: VGPRs
## VGPR-limited kernel reports how far VGPRs must drop for the next level.
RUN: llvm-calc-occupancy -mcpu=gfx950 --vgprs=250 --wg-size=64 | FileCheck %s --check-prefix=NEXT
NEXT: Limited by: VGPRs
NEXT: Next step: reach 3 waves/EU:
NEXT: VGPRs <= 168 (currently 250)
## When several factors tie, all of them must be relaxed.
RUN: llvm-calc-occupancy -mcpu=gfx90a --vgprs=80 --sgprs=102 | FileCheck %s --check-prefix=BOTH
BOTH: Limited by: VGPRs, SGPRs
BOTH: Next step: reach 7 waves/EU (requires all of):
BOTH: VGPRs <= 72 (currently 80)
BOTH: SGPRs <= 96 (currently 102)
## An LDS-limited kernel is told how much LDS to shed.
RUN: llvm-calc-occupancy -mcpu=gfx90a --wg-size=256 --lds=40k | FileCheck %s --check-prefix=NEXTLDS
NEXTLDS: Limited by: workgroup size / LDS
NEXTLDS: Next step: reach 2 waves/EU:
NEXTLDS: LDS <= 32768 bytes (currently 40960)
## No hint when already at the hardware maximum.
RUN: llvm-calc-occupancy -mcpu=gfx90a --vgprs=40 --sgprs=20 --wg-size=64 | FileCheck %s --check-prefix=MAXED
MAXED: Occupancy: 8 waves/EU
MAXED: Next step: already at the hardware maximum
## No workgroup size given: the workgroup dimension is reported as a range.
RUN: llvm-calc-occupancy -mcpu=gfx90a | FileCheck %s --check-prefix=FUZZY
FUZZY: Workgroup size: 1 .. 1024 (unspecified -> full range)
FUZZY: Occupancy: 8 waves/EU (32 waves/CU)
## Requesting more LDS than the GPU has warns and pins occupancy to 1.
RUN: llvm-calc-occupancy -mcpu=gfx90a --lds=103kb 2>&1 | FileCheck %s --check-prefix=BIGLDS
BIGLDS: warning:{{.*}}LDS request (105472 bytes) exceeds addressable LDS
BIGLDS: Occupancy: 1 waves/EU
## The register-limit table can be dumped for a GPU.
RUN: llvm-calc-occupancy -mcpu=gfx90a --limits | FileCheck %s --check-prefix=LIMITS
LIMITS: Occupancy Max VGPRs Max SGPRs
LIMITS: 8 64 80
LIMITS: 1 512 102
## Missing -mcpu is an error.
RUN: not llvm-calc-occupancy 2>&1 | FileCheck %s --check-prefix=NOCPU
NOCPU: error:{{.*}}no GPU specified
## A non-AMDGPU triple is rejected.
RUN: not llvm-calc-occupancy -mtriple=x86_64-- -mcpu=gfx90a 2>&1 | FileCheck %s --check-prefix=BADTRIPLE
BADTRIPLE: error:{{.*}}only supports the AMDGPU target

View file

@ -0,0 +1,30 @@
# This tool depends on AMDGPU backend internals (GCNSubtarget). Only build it
# when the AMDGPU target is enabled.
if(NOT "AMDGPU" IN_LIST LLVM_TARGETS_TO_BUILD)
return()
endif()
include_directories(
${LLVM_MAIN_SRC_DIR}/lib/Target/AMDGPU
${LLVM_BINARY_DIR}/lib/Target/AMDGPU
)
set(LLVM_LINK_COMPONENTS
AMDGPUCodeGen
AMDGPUDesc
AMDGPUInfo
AMDGPUUtils
CodeGen
Core
MC
Support
Target
TargetParser
)
add_llvm_tool(llvm-calc-occupancy
llvm-calc-occupancy.cpp
DEPENDS
AMDGPUCommonTableGen
)

View file

@ -0,0 +1,422 @@
//===-- llvm-calc-occupancy.cpp - AMDGPU occupancy calculator -------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// A small standalone utility that answers "what occupancy do I get?" for an
// AMDGPU kernel, given some subset of its resource usage: workgroup size,
// VGPRs, SGPRs and LDS. Fields that are left unspecified are treated as
// unconstrained, and the result is reported as a range (waves per EU).
//
// It reuses the compiler's own occupancy math (GCNSubtarget) so the numbers
// match what the backend would compute for the same inputs.
//
// TODO: This links the AMDGPU codegen libraries only because the occupancy
// math currently lives in GCNSubtarget. Once that subtarget information is
// exposed through TargetParser, this tool should depend on TargetParser alone
// and drop the codegen dependency.
//
// Example:
// llvm-calc-occupancy -mcpu=gfx90a --wg-size=512 --vgprs=50 --sgprs=30 \
// --lds=103kb
//
//===----------------------------------------------------------------------===//
#include "AMDGPUTargetMachine.h"
#include "GCNSubtarget.h"
#include "Utils/AMDGPUBaseInfo.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/MC/TargetRegistry.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Format.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/TargetSelect.h"
#include "llvm/Support/WithColor.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Target/TargetMachine.h"
#include "llvm/TargetParser/Triple.h"
#include <optional>
using namespace llvm;
namespace {
cl::OptionCategory OccCategory("llvm-calc-occupancy options");
cl::opt<std::string> TripleName("mtriple", cl::desc("Target triple"),
cl::init("amdgpu-amd-amdhsa"),
cl::cat(OccCategory));
cl::opt<std::string> MCPU("mcpu", cl::desc("Target GPU (e.g. gfx90a)"),
cl::init(""), cl::cat(OccCategory));
cl::opt<std::string> MAttr("mattr",
cl::desc("Comma-separated subtarget features "
"(e.g. +wavefrontsize32)"),
cl::init(""), cl::cat(OccCategory));
cl::opt<std::string>
WGSizeStr("wg-size",
cl::desc("Flat workgroup size: single value 'N' or range "
"'MIN:MAX' (default: 1:1024)"),
cl::init(""), cl::cat(OccCategory));
cl::alias WGSizeAlias("flat-workgroup-size", cl::aliasopt(WGSizeStr));
cl::opt<int> NumVGPRs("vgprs", cl::desc("VGPRs used per lane (default: none)"),
cl::init(-1), cl::cat(OccCategory));
cl::opt<int> NumSGPRs("sgprs", cl::desc("SGPRs used per wave (default: none)"),
cl::init(-1), cl::cat(OccCategory));
cl::opt<std::string> LDSStr("lds",
cl::desc("LDS bytes per workgroup, accepts k/kb/m "
"suffixes (default: 0)"),
cl::init(""), cl::cat(OccCategory));
cl::opt<unsigned>
DynVGPRBlockSize("dynamic-vgpr-block-size",
cl::desc("Dynamic VGPR block size (0 = disabled)"),
cl::init(0), cl::cat(OccCategory));
cl::opt<bool> ShowLimits("limits",
cl::desc("Print the per-occupancy VGPR/SGPR limit "
"table for this GPU"),
cl::init(false), cl::cat(OccCategory));
} // namespace
// Parse a byte size with an optional binary suffix (k/kb/kib/m/mb/mib), all
// base 1024. A bare number is interpreted as bytes.
static bool parseSize(StringRef S, uint64_t &Out) {
S = S.trim();
if (S.empty())
return false;
uint64_t Mult = 1;
static const std::pair<StringRef, uint64_t> Suffixes[] = {
{"kib", 1024}, {"kb", 1024}, {"k", 1024},
{"mib", 1024 * 1024}, {"mb", 1024 * 1024}, {"m", 1024 * 1024}};
for (const auto &[Suf, M] : Suffixes) {
if (S.take_back(Suf.size()).equals_insensitive(Suf)) {
Mult = M;
S = S.drop_back(Suf.size()).rtrim();
break;
}
}
uint64_t Value;
if (S.getAsInteger(10, Value))
return false;
Out = Value * Mult;
return true;
}
// Parse "N" or "MIN:MAX" (also accepts "MIN-MAX") into a flat workgroup range.
static bool parseWGRange(StringRef S, unsigned &Min, unsigned &Max) {
S = S.trim();
StringRef LHS, RHS;
if (S.contains(':'))
std::tie(LHS, RHS) = S.split(':');
else if (S.contains('-'))
std::tie(LHS, RHS) = S.split('-');
else
LHS = RHS = S;
unsigned Lo, Hi;
if (LHS.trim().getAsInteger(10, Lo) || RHS.trim().getAsInteger(10, Hi))
return false;
if (Lo == 0 || Hi == 0 || Lo > Hi)
return false;
Min = Lo;
Max = Hi;
return true;
}
static std::string formatBytes(uint64_t Bytes) {
if (Bytes && Bytes % 1024 == 0)
return (Twine(Bytes) + " bytes (" + Twine(Bytes / 1024) + " KiB)").str();
return (Twine(Bytes) + " bytes").str();
}
int main(int argc, char **argv) {
InitLLVM X(argc, argv);
const char *ToolName = argv[0];
cl::HideUnrelatedOptions(OccCategory);
cl::ParseCommandLineOptions(
argc, argv,
"AMDGPU occupancy calculator\n\n"
" Prints the occupancy (waves per EU) implied by a given workgroup "
"size,\n"
" VGPR/SGPR usage and LDS allocation. Unspecified fields are reported "
"as\n"
" a range.\n");
LLVMInitializeAMDGPUTargetInfo();
LLVMInitializeAMDGPUTarget();
LLVMInitializeAMDGPUTargetMC();
if (MCPU.empty()) {
WithColor::error(errs(), ToolName)
<< "no GPU specified; pass -mcpu=<gfxNNN> (e.g. -mcpu=gfx90a)\n";
return 1;
}
Triple TT(Triple::normalize(TripleName));
if (!TT.isAMDGCN()) {
WithColor::error(errs(), ToolName)
<< "this tool only supports the AMDGPU target; got triple '" << TT.str()
<< "'\n";
return 1;
}
std::string Error;
const Target *T = TargetRegistry::lookupTarget(TT, Error);
if (!T) {
WithColor::error(errs(), ToolName) << Error << "\n";
return 1;
}
TargetOptions Options;
std::unique_ptr<TargetMachine> TM(T->createTargetMachine(
TT, MCPU, MAttr, Options, std::nullopt, std::nullopt));
if (!TM) {
WithColor::error(errs(), ToolName)
<< "failed to create target machine for '" << MCPU << "'\n";
return 1;
}
GCNSubtarget ST(TM->getTargetTriple(), std::string(TM->getTargetCPU()),
std::string(TM->getTargetFeatureString()),
*static_cast<GCNTargetMachine *>(TM.get()));
const MCSubtargetInfo &STI = ST;
// Mirror GCNSubtarget::computeOccupancy: when the block size is not given
// explicitly, fall back to the subtarget's default if dynamic VGPRs are on.
unsigned DynVGPRBlockSizeEff = DynVGPRBlockSize;
if (DynVGPRBlockSizeEff == 0 && ST.isDynamicVGPREnabled())
DynVGPRBlockSizeEff = ST.getDynamicVGPRBlockSize();
// Parse inputs.
unsigned WGMin = 1, WGMax = AMDGPU::IsaInfo::getMaxFlatWorkGroupSize();
bool WGSpecified = !WGSizeStr.empty();
if (WGSpecified && !parseWGRange(WGSizeStr, WGMin, WGMax)) {
WithColor::error(errs(), ToolName)
<< "invalid --wg-size '" << WGSizeStr << "'\n";
return 1;
}
uint64_t LDSBytes = 0;
bool LDSSpecified = !LDSStr.empty();
if (LDSSpecified && !parseSize(LDSStr, LDSBytes)) {
WithColor::error(errs(), ToolName) << "invalid --lds '" << LDSStr << "'\n";
return 1;
}
bool VGPRSpecified = NumVGPRs >= 0;
bool SGPRSpecified = NumSGPRs >= 0;
// Hardware characteristics.
unsigned WaveSize = AMDGPU::IsaInfo::getWavefrontSize(STI);
unsigned MaxWaves = AMDGPU::IsaInfo::getMaxWavesPerEU(STI);
unsigned EUsPerCU = AMDGPU::IsaInfo::getEUsPerCU(STI);
unsigned LocalMemSize = AMDGPU::IsaInfo::getLocalMemorySize(STI);
unsigned AddrLocalMem = AMDGPU::IsaInfo::getAddressableLocalMemorySize(STI);
unsigned AddrVGPRs =
AMDGPU::IsaInfo::getAddressableNumVGPRs(STI, DynVGPRBlockSizeEff);
unsigned AddrSGPRs = AMDGPU::IsaInfo::getAddressableNumSGPRs(STI);
unsigned MaxWGSize = AMDGPU::IsaInfo::getMaxFlatWorkGroupSize();
// Warn about inputs that exceed the hardware's physical capacity: such a
// kernel could not actually launch, so the reported occupancy is only the
// math extrapolated past the limit.
auto Warn = [ToolName](const Twine &Msg) {
WithColor::warning(errs(), ToolName) << Msg << "\n";
};
if (LDSSpecified && LDSBytes > AddrLocalMem)
Warn("LDS request (" + Twine(LDSBytes) +
" bytes) exceeds addressable LDS "
"per workgroup (" +
Twine(AddrLocalMem) + " bytes)");
if (VGPRSpecified && static_cast<unsigned>(NumVGPRs) > AddrVGPRs)
Warn("VGPR request (" + Twine(static_cast<int>(NumVGPRs)) +
") exceeds addressable "
"VGPRs (" +
Twine(AddrVGPRs) + ")");
if (SGPRSpecified && static_cast<unsigned>(NumSGPRs) > AddrSGPRs)
Warn("SGPR request (" + Twine(static_cast<int>(NumSGPRs)) +
") exceeds addressable "
"SGPRs (" +
Twine(AddrSGPRs) + ")");
if (WGMax > MaxWGSize)
Warn("workgroup size (" + Twine(WGMax) +
") exceeds the maximum flat "
"workgroup size (" +
Twine(MaxWGSize) + ")");
outs() << "llvm-calc-occupancy - AMDGPU occupancy calculator\n\n";
outs() << "Target\n";
outs() << format(" %-20s %s\n", "Triple:", TT.str().c_str());
outs() << format(" %-20s %s\n",
"GPU (-mcpu):", std::string(TM->getTargetCPU()).c_str());
outs() << format(" %-20s %u\n", "Wavefront size:", WaveSize);
outs() << format(" %-20s %u (waves per SIMD, hardware limit)\n",
"Max waves/EU:", MaxWaves);
outs() << format(" %-20s %u\n", "EUs (SIMDs) per CU:", EUsPerCU);
outs() << format(" %-20s %s\n",
"LDS per CU:", formatBytes(LocalMemSize).c_str());
outs() << format(" %-20s %s\n", "Addressable LDS:",
(formatBytes(AddrLocalMem) + " per workgroup").c_str());
outs() << "\nInputs\n";
if (WGSpecified) {
if (WGMin == WGMax)
outs() << format(" %-20s %u\n", "Workgroup size:", WGMin);
else
outs() << format(" %-20s %u .. %u\n", "Workgroup size:", WGMin, WGMax);
} else {
outs() << format(" %-20s %u .. %u (unspecified -> full range)\n",
"Workgroup size:", WGMin, WGMax);
}
if (VGPRSpecified)
outs() << format(" %-20s %d\n",
"VGPRs per lane:", static_cast<int>(NumVGPRs));
else
outs() << format(" %-20s %s\n", "VGPRs per lane:", "unspecified");
if (SGPRSpecified)
outs() << format(" %-20s %d\n",
"SGPRs per wave:", static_cast<int>(NumSGPRs));
else
outs() << format(" %-20s %s\n", "SGPRs per wave:", "unspecified");
if (LDSSpecified)
outs() << format(" %-20s %s\n",
"LDS per workgroup:", formatBytes(LDSBytes).c_str());
else
outs() << format(" %-20s %s\n", "LDS per workgroup:", "unspecified (0)");
// Per-constraint occupancy (all in waves/EU).
auto [WGMinOcc, WGMaxOcc] = ST.getOccupancyWithWorkGroupSizes(
static_cast<uint32_t>(LDSBytes), {WGMin, WGMax});
unsigned VGPROcc =
VGPRSpecified ? ST.getOccupancyWithNumVGPRs(NumVGPRs, DynVGPRBlockSizeEff)
: MaxWaves;
unsigned SGPROcc =
SGPRSpecified ? ST.getOccupancyWithNumSGPRs(NumSGPRs) : MaxWaves;
outs() << "\nPer-constraint occupancy (waves/EU)\n";
if (WGMinOcc == WGMaxOcc)
outs() << format(" %-20s %u\n", "Workgroup + LDS:", WGMaxOcc);
else
outs() << format(" %-20s %u .. %u\n", "Workgroup + LDS:", WGMinOcc,
WGMaxOcc);
if (VGPRSpecified)
outs() << format(" %-20s %u\n", "VGPRs:", VGPROcc);
if (SGPRSpecified)
outs() << format(" %-20s %u\n", "SGPRs:", SGPROcc);
// Combine like GCNSubtarget::computeOccupancy.
unsigned MaxOcc = std::min({WGMaxOcc, VGPROcc, SGPROcc});
unsigned MinOcc = std::min(WGMinOcc, MaxOcc);
// Identify what pins the maximum occupancy.
SmallVector<StringRef, 3> LimitedBy;
if (WGMaxOcc == MaxOcc)
LimitedBy.push_back("workgroup size / LDS");
if (VGPRSpecified && VGPROcc == MaxOcc)
LimitedBy.push_back("VGPRs");
if (SGPRSpecified && SGPROcc == MaxOcc)
LimitedBy.push_back("SGPRs");
outs() << "\nResult\n";
if (MinOcc == MaxOcc)
outs() << format(" %-20s %u waves/EU (%u waves/CU)\n",
"Occupancy:", MaxOcc, MaxOcc * EUsPerCU);
else
outs() << format(" %-20s %u .. %u waves/EU (%u .. %u waves/CU)\n",
"Occupancy:", MinOcc, MaxOcc, MinOcc * EUsPerCU,
MaxOcc * EUsPerCU);
outs() << format(" %-20s %s\n",
"Limited by:", join(LimitedBy, ", ").c_str());
// Hint: what would it take to gain one more wave/EU. Every factor that
// currently pins the occupancy has to be relaxed, so list each one.
if (MaxOcc >= MaxWaves) {
outs() << format(" %-20s already at the hardware maximum\n", "Next step:");
} else {
unsigned TargetOcc = MaxOcc + 1;
outs() << format(" %-20s reach %u waves/EU%s\n", "Next step:", TargetOcc,
LimitedBy.size() > 1 ? " (requires all of):" : ":");
if (VGPRSpecified && VGPROcc == MaxOcc) {
unsigned MaxV =
AMDGPU::IsaInfo::getMaxNumVGPRs(STI, TargetOcc, DynVGPRBlockSizeEff);
outs() << format(" VGPRs <= %u (currently %d)\n", MaxV,
static_cast<int>(NumVGPRs));
}
if (SGPRSpecified && SGPROcc == MaxOcc) {
unsigned MaxS =
AMDGPU::IsaInfo::getMaxNumSGPRs(STI, TargetOcc, /*Addressable=*/true);
outs() << format(" SGPRs <= %u (currently %d)\n", MaxS,
static_cast<int>(NumSGPRs));
}
if (WGMaxOcc == MaxOcc) {
auto WGLDSOcc = [&](uint32_t LDS, unsigned Lo, unsigned Hi) {
return ST.getOccupancyWithWorkGroupSizes(LDS, {Lo, Hi}).second;
};
uint32_t LDS32 = static_cast<uint32_t>(LDSBytes);
bool Suggested = false;
// LDS lever: largest LDS that still reaches TargetOcc at current WG.
if (LDSSpecified && LDSBytes > 0 &&
WGLDSOcc(0, WGMin, WGMax) >= TargetOcc) {
uint64_t Lo = 0, Hi = LDSBytes;
while (Lo < Hi) {
uint64_t Mid = Lo + (Hi - Lo + 1) / 2;
if (WGLDSOcc(static_cast<uint32_t>(Mid), WGMin, WGMax) >= TargetOcc)
Lo = Mid;
else
Hi = Mid - 1;
}
outs() << format(" LDS <= %llu bytes (currently %llu)\n",
static_cast<unsigned long long>(Lo),
static_cast<unsigned long long>(LDSBytes));
Suggested = true;
}
// Workgroup lever: largest flat workgroup size that reaches TargetOcc.
if (WGMax > 1 && WGLDSOcc(LDS32, 1, 1) >= TargetOcc) {
unsigned Lo = 1, Hi = WGMax;
while (Lo < Hi) {
unsigned Mid = Lo + (Hi - Lo + 1) / 2;
if (WGLDSOcc(LDS32, Mid, Mid) >= TargetOcc)
Lo = Mid;
else
Hi = Mid - 1;
}
if (Lo < WGMax) {
outs() << format(" workgroup size <= %u (currently %u)\n", Lo,
WGMax);
Suggested = true;
}
}
if (!Suggested)
outs() << " reduce workgroup size and/or LDS\n";
}
}
if (ShowLimits) {
outs() << "\nPer-occupancy register limits (max regs to still reach "
"each level)\n";
outs() << format(" %-14s %-14s %-14s\n", "Occupancy", "Max VGPRs",
"Max SGPRs");
for (unsigned Occ = MaxWaves; Occ >= 1; --Occ) {
unsigned MaxV =
AMDGPU::IsaInfo::getMaxNumVGPRs(STI, Occ, DynVGPRBlockSizeEff);
unsigned MaxS =
AMDGPU::IsaInfo::getMaxNumSGPRs(STI, Occ, /*Addressable=*/true);
outs() << format(" %-14u %-14u %-14u\n", Occ, MaxV, MaxS);
}
}
return 0;
}