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

via llvm-commits llvm-commits at lists.llvm.org
Thu Jul 30 06:11:38 PDT 2026


Author: michaelselehov
Date: 2026-07-30T15:11:33+02:00
New Revision: b11722f1d3bd5debac949fe366c1801d39b8a2bb

URL: https://github.com/llvm/llvm-project/commit/b11722f1d3bd5debac949fe366c1801d39b8a2bb
DIFF: https://github.com/llvm/llvm-project/commit/b11722f1d3bd5debac949fe366c1801d39b8a2bb.diff

LOG: [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 at amd.com>

Added: 
    llvm/docs/CommandGuide/llvm-calc-occupancy.rst
    llvm/test/tools/llvm-calc-occupancy/basic.test
    llvm/tools/llvm-calc-occupancy/CMakeLists.txt
    llvm/tools/llvm-calc-occupancy/llvm-calc-occupancy.cpp

Modified: 
    llvm/docs/CommandGuide/index.md
    llvm/test/CMakeLists.txt
    llvm/test/lit.cfg.py

Removed: 
    


################################################################################
diff  --git a/llvm/docs/CommandGuide/index.md b/llvm/docs/CommandGuide/index.md
index ac90d9468b848..b9c14f30c3010 100644
--- a/llvm/docs/CommandGuide/index.md
+++ b/llvm/docs/CommandGuide/index.md
@@ -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
 

diff  --git a/llvm/docs/CommandGuide/llvm-calc-occupancy.rst b/llvm/docs/CommandGuide/llvm-calc-occupancy.rst
new file mode 100644
index 0000000000000..72f972ccae097
--- /dev/null
+++ b/llvm/docs/CommandGuide/llvm-calc-occupancy.rst
@@ -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).

diff  --git a/llvm/test/CMakeLists.txt b/llvm/test/CMakeLists.txt
index 201f0ee86d7ff..3f1ee7c18bde7 100644
--- a/llvm/test/CMakeLists.txt
+++ b/llvm/test/CMakeLists.txt
@@ -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

diff  --git a/llvm/test/lit.cfg.py b/llvm/test/lit.cfg.py
index c77687bf2c8af..f48c3a57a89aa 100644
--- a/llvm/test/lit.cfg.py
+++ b/llvm/test/lit.cfg.py
@@ -341,6 +341,7 @@ def get_asan_rtlib():
         ToolSubst("OrcV2CBindingsLazy", unresolved="ignore"),
         ToolSubst("OrcV2CBindingsVeryLazy", unresolved="ignore"),
         ToolSubst("dxil-dis", unresolved="ignore"),
+        ToolSubst("llvm-calc-occupancy", unresolved="ignore"),
     ]
 )
 

diff  --git a/llvm/test/tools/llvm-calc-occupancy/basic.test b/llvm/test/tools/llvm-calc-occupancy/basic.test
new file mode 100644
index 0000000000000..3c2f63229df59
--- /dev/null
+++ b/llvm/test/tools/llvm-calc-occupancy/basic.test
@@ -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

diff  --git a/llvm/tools/llvm-calc-occupancy/CMakeLists.txt b/llvm/tools/llvm-calc-occupancy/CMakeLists.txt
new file mode 100644
index 0000000000000..9580d855f06a3
--- /dev/null
+++ b/llvm/tools/llvm-calc-occupancy/CMakeLists.txt
@@ -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
+  )

diff  --git a/llvm/tools/llvm-calc-occupancy/llvm-calc-occupancy.cpp b/llvm/tools/llvm-calc-occupancy/llvm-calc-occupancy.cpp
new file mode 100644
index 0000000000000..916c9fb941626
--- /dev/null
+++ b/llvm/tools/llvm-calc-occupancy/llvm-calc-occupancy.cpp
@@ -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;
+}


        


More information about the llvm-commits mailing list