1//===-- llvm-calc-occupancy.cpp - AMDGPU occupancy calculator -------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// A small standalone utility that answers "what occupancy do I get?" for an
10// AMDGPU kernel, given some subset of its resource usage: workgroup size,
11// VGPRs, SGPRs and LDS. Fields that are left unspecified are treated as
12// unconstrained, and the result is reported as a range (waves per EU).
13//
14// It reuses the compiler's own occupancy math (GCNSubtarget) so the numbers
15// match what the backend would compute for the same inputs.
16//
17// TODO: This links the AMDGPU codegen libraries only because the occupancy
18// math currently lives in GCNSubtarget. Once that subtarget information is
19// exposed through TargetParser, this tool should depend on TargetParser alone
20// and drop the codegen dependency.
21//
22// Example:
23// llvm-calc-occupancy -mcpu=gfx90a --wg-size=512 --vgprs=50 --sgprs=30 \
24// --lds=103kb
25//
26//===----------------------------------------------------------------------===//
27
28#include "AMDGPUTargetMachine.h"
29#include "GCNSubtarget.h"
30#include "Utils/AMDGPUBaseInfo.h"
31#include "llvm/ADT/SmallVector.h"
32#include "llvm/ADT/StringExtras.h"
33#include "llvm/ADT/StringRef.h"
34#include "llvm/MC/TargetRegistry.h"
35#include "llvm/Support/CommandLine.h"
36#include "llvm/Support/Format.h"
37#include "llvm/Support/InitLLVM.h"
38#include "llvm/Support/TargetSelect.h"
39#include "llvm/Support/WithColor.h"
40#include "llvm/Support/raw_ostream.h"
41#include "llvm/Target/TargetMachine.h"
42#include "llvm/TargetParser/Triple.h"
43#include <optional>
44
45using namespace llvm;
46
47namespace {
48cl::OptionCategory OccCategory("llvm-calc-occupancy options");
49
50cl::opt<std::string> TripleName("mtriple", cl::desc("Target triple"),
51 cl::init(Val: "amdgpu-amd-amdhsa"),
52 cl::cat(OccCategory));
53
54cl::opt<std::string> MCPU("mcpu", cl::desc("Target GPU (e.g. gfx90a)"),
55 cl::init(Val: ""), cl::cat(OccCategory));
56
57cl::opt<std::string> MAttr("mattr",
58 cl::desc("Comma-separated subtarget features "
59 "(e.g. +wavefrontsize32)"),
60 cl::init(Val: ""), cl::cat(OccCategory));
61
62cl::opt<std::string>
63 WGSizeStr("wg-size",
64 cl::desc("Flat workgroup size: single value 'N' or range "
65 "'MIN:MAX' (default: 1:1024)"),
66 cl::init(Val: ""), cl::cat(OccCategory));
67cl::alias WGSizeAlias("flat-workgroup-size", cl::aliasopt(WGSizeStr));
68
69cl::opt<int> NumVGPRs("vgprs", cl::desc("VGPRs used per lane (default: none)"),
70 cl::init(Val: -1), cl::cat(OccCategory));
71
72cl::opt<int> NumSGPRs("sgprs", cl::desc("SGPRs used per wave (default: none)"),
73 cl::init(Val: -1), cl::cat(OccCategory));
74
75cl::opt<std::string> LDSStr("lds",
76 cl::desc("LDS bytes per workgroup, accepts k/kb/m "
77 "suffixes (default: 0)"),
78 cl::init(Val: ""), cl::cat(OccCategory));
79
80cl::opt<unsigned>
81 DynVGPRBlockSize("dynamic-vgpr-block-size",
82 cl::desc("Dynamic VGPR block size (0 = disabled)"),
83 cl::init(Val: 0), cl::cat(OccCategory));
84
85cl::opt<bool> ShowLimits("limits",
86 cl::desc("Print the per-occupancy VGPR/SGPR limit "
87 "table for this GPU"),
88 cl::init(Val: false), cl::cat(OccCategory));
89} // namespace
90
91// Parse a byte size with an optional binary suffix (k/kb/kib/m/mb/mib), all
92// base 1024. A bare number is interpreted as bytes.
93static bool parseSize(StringRef S, uint64_t &Out) {
94 S = S.trim();
95 if (S.empty())
96 return false;
97 uint64_t Mult = 1;
98 static const std::pair<StringRef, uint64_t> Suffixes[] = {
99 {"kib", 1024}, {"kb", 1024}, {"k", 1024},
100 {"mib", 1024 * 1024}, {"mb", 1024 * 1024}, {"m", 1024 * 1024}};
101 for (const auto &[Suf, M] : Suffixes) {
102 if (S.take_back(N: Suf.size()).equals_insensitive(RHS: Suf)) {
103 Mult = M;
104 S = S.drop_back(N: Suf.size()).rtrim();
105 break;
106 }
107 }
108 uint64_t Value;
109 if (S.getAsInteger(Radix: 10, Result&: Value))
110 return false;
111 Out = Value * Mult;
112 return true;
113}
114
115// Parse "N" or "MIN:MAX" (also accepts "MIN-MAX") into a flat workgroup range.
116static bool parseWGRange(StringRef S, unsigned &Min, unsigned &Max) {
117 S = S.trim();
118 StringRef LHS, RHS;
119 if (S.contains(C: ':'))
120 std::tie(args&: LHS, args&: RHS) = S.split(Separator: ':');
121 else if (S.contains(C: '-'))
122 std::tie(args&: LHS, args&: RHS) = S.split(Separator: '-');
123 else
124 LHS = RHS = S;
125
126 unsigned Lo, Hi;
127 if (LHS.trim().getAsInteger(Radix: 10, Result&: Lo) || RHS.trim().getAsInteger(Radix: 10, Result&: Hi))
128 return false;
129 if (Lo == 0 || Hi == 0 || Lo > Hi)
130 return false;
131 Min = Lo;
132 Max = Hi;
133 return true;
134}
135
136static std::string formatBytes(uint64_t Bytes) {
137 if (Bytes && Bytes % 1024 == 0)
138 return (Twine(Bytes) + " bytes (" + Twine(Bytes / 1024) + " KiB)").str();
139 return (Twine(Bytes) + " bytes").str();
140}
141
142int main(int argc, char **argv) {
143 InitLLVM X(argc, argv);
144 const char *ToolName = argv[0];
145
146 cl::HideUnrelatedOptions(Category&: OccCategory);
147 cl::ParseCommandLineOptions(
148 argc, argv,
149 Overview: "AMDGPU occupancy calculator\n\n"
150 " Prints the occupancy (waves per EU) implied by a given workgroup "
151 "size,\n"
152 " VGPR/SGPR usage and LDS allocation. Unspecified fields are reported "
153 "as\n"
154 " a range.\n");
155
156 LLVMInitializeAMDGPUTargetInfo();
157 LLVMInitializeAMDGPUTarget();
158 LLVMInitializeAMDGPUTargetMC();
159
160 if (MCPU.empty()) {
161 WithColor::error(OS&: errs(), Prefix: ToolName)
162 << "no GPU specified; pass -mcpu=<gfxNNN> (e.g. -mcpu=gfx90a)\n";
163 return 1;
164 }
165
166 Triple TT(Triple::normalize(Str: TripleName));
167 if (!TT.isAMDGCN()) {
168 WithColor::error(OS&: errs(), Prefix: ToolName)
169 << "this tool only supports the AMDGPU target; got triple '" << TT.str()
170 << "'\n";
171 return 1;
172 }
173
174 std::string Error;
175 const Target *T = TargetRegistry::lookupTarget(TheTriple: TT, Error);
176 if (!T) {
177 WithColor::error(OS&: errs(), Prefix: ToolName) << Error << "\n";
178 return 1;
179 }
180
181 TargetOptions Options;
182 std::unique_ptr<TargetMachine> TM(T->createTargetMachine(
183 TT, CPU: MCPU, Features: MAttr, Options, RM: std::nullopt, CM: std::nullopt));
184 if (!TM) {
185 WithColor::error(OS&: errs(), Prefix: ToolName)
186 << "failed to create target machine for '" << MCPU << "'\n";
187 return 1;
188 }
189
190 GCNSubtarget ST(TM->getTargetTriple(), std::string(TM->getTargetCPU()),
191 std::string(TM->getTargetFeatureString()),
192 *static_cast<GCNTargetMachine *>(TM.get()));
193
194 const MCSubtargetInfo &STI = ST;
195
196 // Parse inputs.
197 unsigned WGMin = 1, WGMax = AMDGPU::IsaInfo::getMaxFlatWorkGroupSize();
198 bool WGSpecified = !WGSizeStr.empty();
199 if (WGSpecified && !parseWGRange(S: WGSizeStr, Min&: WGMin, Max&: WGMax)) {
200 WithColor::error(OS&: errs(), Prefix: ToolName)
201 << "invalid --wg-size '" << WGSizeStr << "'\n";
202 return 1;
203 }
204
205 uint64_t LDSBytes = 0;
206 bool LDSSpecified = !LDSStr.empty();
207 if (LDSSpecified && !parseSize(S: LDSStr, Out&: LDSBytes)) {
208 WithColor::error(OS&: errs(), Prefix: ToolName) << "invalid --lds '" << LDSStr << "'\n";
209 return 1;
210 }
211
212 bool VGPRSpecified = NumVGPRs >= 0;
213 bool SGPRSpecified = NumSGPRs >= 0;
214
215 // Hardware characteristics.
216 unsigned WaveSize = AMDGPU::IsaInfo::getWavefrontSize(STI);
217 unsigned MaxWaves = ST.getMaxWavesPerEU();
218 unsigned NumWorkGroupSIMDs = ST.getNumWorkGroupSIMDs();
219 unsigned LocalMemSize = AMDGPU::IsaInfo::getLocalMemorySize(STI);
220 unsigned AddrLocalMem = AMDGPU::IsaInfo::getAddressableLocalMemorySize(STI);
221 unsigned AddrVGPRs =
222 AMDGPU::IsaInfo::getAddressableNumVGPRs(STI, DynamicVGPRBlockSize: DynVGPRBlockSize);
223 unsigned AddrSGPRs = ST.getAddressableNumSGPRs();
224 unsigned MaxWGSize = AMDGPU::IsaInfo::getMaxFlatWorkGroupSize();
225
226 // Warn about inputs that exceed the hardware's physical capacity: such a
227 // kernel could not actually launch, so the reported occupancy is only the
228 // math extrapolated past the limit.
229 auto Warn = [ToolName](const Twine &Msg) {
230 WithColor::warning(OS&: errs(), Prefix: ToolName) << Msg << "\n";
231 };
232 if (LDSSpecified && LDSBytes > AddrLocalMem)
233 Warn("LDS request (" + Twine(LDSBytes) +
234 " bytes) exceeds addressable LDS "
235 "per workgroup (" +
236 Twine(AddrLocalMem) + " bytes)");
237 if (VGPRSpecified && static_cast<unsigned>(NumVGPRs) > AddrVGPRs)
238 Warn("VGPR request (" + Twine(static_cast<int>(NumVGPRs)) +
239 ") exceeds addressable "
240 "VGPRs (" +
241 Twine(AddrVGPRs) + ")");
242 if (SGPRSpecified && static_cast<unsigned>(NumSGPRs) > AddrSGPRs)
243 Warn("SGPR request (" + Twine(static_cast<int>(NumSGPRs)) +
244 ") exceeds addressable "
245 "SGPRs (" +
246 Twine(AddrSGPRs) + ")");
247 if (WGMax > MaxWGSize)
248 Warn("workgroup size (" + Twine(WGMax) +
249 ") exceeds the maximum flat "
250 "workgroup size (" +
251 Twine(MaxWGSize) + ")");
252
253 outs() << "llvm-calc-occupancy - AMDGPU occupancy calculator\n\n";
254 outs() << "Target\n";
255 outs() << format(Fmt: " %-20s %s\n", Vals: "Triple:", Vals: TT.str().c_str());
256 outs() << format(Fmt: " %-20s %s\n",
257 Vals: "GPU (-mcpu):", Vals: std::string(TM->getTargetCPU()).c_str());
258 outs() << format(Fmt: " %-20s %u\n", Vals: "Wavefront size:", Vals: WaveSize);
259 outs() << format(Fmt: " %-20s %u (waves per SIMD, hardware limit)\n",
260 Vals: "Max waves/EU:", Vals: MaxWaves);
261 outs() << format(Fmt: " %-20s %u\n", Vals: "SIMDs/work-group:", Vals: NumWorkGroupSIMDs);
262 outs() << format(Fmt: " %-20s %s\n",
263 Vals: "LDS per CU:", Vals: formatBytes(Bytes: LocalMemSize).c_str());
264 outs() << format(Fmt: " %-20s %s\n", Vals: "Addressable LDS:",
265 Vals: (formatBytes(Bytes: AddrLocalMem) + " per workgroup").c_str());
266
267 outs() << "\nInputs\n";
268 if (WGSpecified) {
269 if (WGMin == WGMax)
270 outs() << format(Fmt: " %-20s %u\n", Vals: "Workgroup size:", Vals: WGMin);
271 else
272 outs() << format(Fmt: " %-20s %u .. %u\n", Vals: "Workgroup size:", Vals: WGMin, Vals: WGMax);
273 } else {
274 outs() << format(Fmt: " %-20s %u .. %u (unspecified -> full range)\n",
275 Vals: "Workgroup size:", Vals: WGMin, Vals: WGMax);
276 }
277 if (VGPRSpecified)
278 outs() << format(Fmt: " %-20s %d\n",
279 Vals: "VGPRs per lane:", Vals: static_cast<int>(NumVGPRs));
280 else
281 outs() << format(Fmt: " %-20s %s\n", Vals: "VGPRs per lane:", Vals: "unspecified");
282 if (SGPRSpecified)
283 outs() << format(Fmt: " %-20s %d\n",
284 Vals: "SGPRs per wave:", Vals: static_cast<int>(NumSGPRs));
285 else
286 outs() << format(Fmt: " %-20s %s\n", Vals: "SGPRs per wave:", Vals: "unspecified");
287 if (LDSSpecified)
288 outs() << format(Fmt: " %-20s %s\n",
289 Vals: "LDS per workgroup:", Vals: formatBytes(Bytes: LDSBytes).c_str());
290 else
291 outs() << format(Fmt: " %-20s %s\n", Vals: "LDS per workgroup:", Vals: "unspecified (0)");
292
293 // Per-constraint occupancy (all in waves/EU).
294 auto [WGMinOcc, WGMaxOcc] = ST.getOccupancyWithWorkGroupSizes(
295 LDSBytes: static_cast<uint32_t>(LDSBytes), FlatWorkGroupSizes: {WGMin, WGMax});
296 unsigned VGPROcc =
297 VGPRSpecified ? ST.getOccupancyWithNumVGPRs(VGPRs: NumVGPRs, DynamicVGPRBlockSize: DynVGPRBlockSize)
298 : MaxWaves;
299 unsigned SGPROcc =
300 SGPRSpecified ? ST.getOccupancyWithNumSGPRs(SGPRs: NumSGPRs) : MaxWaves;
301
302 outs() << "\nPer-constraint occupancy (waves/EU)\n";
303 if (WGMinOcc == WGMaxOcc)
304 outs() << format(Fmt: " %-20s %u\n", Vals: "Workgroup + LDS:", Vals: WGMaxOcc);
305 else
306 outs() << format(Fmt: " %-20s %u .. %u\n", Vals: "Workgroup + LDS:", Vals: WGMinOcc,
307 Vals: WGMaxOcc);
308 if (VGPRSpecified)
309 outs() << format(Fmt: " %-20s %u\n", Vals: "VGPRs:", Vals: VGPROcc);
310 if (SGPRSpecified)
311 outs() << format(Fmt: " %-20s %u\n", Vals: "SGPRs:", Vals: SGPROcc);
312
313 // Combine like GCNSubtarget::computeOccupancy.
314 unsigned MaxOcc = std::min(l: {WGMaxOcc, VGPROcc, SGPROcc});
315 unsigned MinOcc = std::min(a: WGMinOcc, b: MaxOcc);
316
317 // Identify what pins the maximum occupancy.
318 SmallVector<StringRef, 3> LimitedBy;
319 if (WGMaxOcc == MaxOcc)
320 LimitedBy.push_back(Elt: "workgroup size / LDS");
321 if (VGPRSpecified && VGPROcc == MaxOcc)
322 LimitedBy.push_back(Elt: "VGPRs");
323 if (SGPRSpecified && SGPROcc == MaxOcc)
324 LimitedBy.push_back(Elt: "SGPRs");
325
326 outs() << "\nResult\n";
327 if (MinOcc == MaxOcc)
328 outs() << format(Fmt: " %-20s %u waves/EU (%u waves/CU)\n",
329 Vals: "Occupancy:", Vals: MaxOcc, Vals: MaxOcc * NumWorkGroupSIMDs);
330 else
331 outs() << format(Fmt: " %-20s %u .. %u waves/EU (%u .. %u waves/CU)\n",
332 Vals: "Occupancy:", Vals: MinOcc, Vals: MaxOcc, Vals: MinOcc * NumWorkGroupSIMDs,
333 Vals: MaxOcc * NumWorkGroupSIMDs);
334 outs() << format(Fmt: " %-20s %s\n",
335 Vals: "Limited by:", Vals: join(R&: LimitedBy, Separator: ", ").c_str());
336
337 // Hint: what would it take to gain one more wave/EU. Every factor that
338 // currently pins the occupancy has to be relaxed, so list each one.
339 if (MaxOcc >= MaxWaves) {
340 outs() << format(Fmt: " %-20s already at the hardware maximum\n", Vals: "Next step:");
341 } else {
342 unsigned TargetOcc = MaxOcc + 1;
343 outs() << format(Fmt: " %-20s reach %u waves/EU%s\n", Vals: "Next step:", Vals: TargetOcc,
344 Vals: LimitedBy.size() > 1 ? " (requires all of):" : ":");
345
346 if (VGPRSpecified && VGPROcc == MaxOcc) {
347 unsigned MaxV =
348 AMDGPU::IsaInfo::getMaxNumVGPRs(STI, WavesPerEU: TargetOcc, DynamicVGPRBlockSize: DynVGPRBlockSize);
349 outs() << format(Fmt: " VGPRs <= %u (currently %d)\n", Vals: MaxV,
350 Vals: static_cast<int>(NumVGPRs));
351 }
352 if (SGPRSpecified && SGPROcc == MaxOcc) {
353 unsigned MaxS =
354 AMDGPU::IsaInfo::getMaxNumSGPRs(STI, WavesPerEU: TargetOcc, /*Addressable=*/true);
355 outs() << format(Fmt: " SGPRs <= %u (currently %d)\n", Vals: MaxS,
356 Vals: static_cast<int>(NumSGPRs));
357 }
358 if (WGMaxOcc == MaxOcc) {
359 auto WGLDSOcc = [&](uint32_t LDS, unsigned Lo, unsigned Hi) {
360 return ST.getOccupancyWithWorkGroupSizes(LDSBytes: LDS, FlatWorkGroupSizes: {Lo, Hi}).second;
361 };
362 uint32_t LDS32 = static_cast<uint32_t>(LDSBytes);
363 bool Suggested = false;
364 // LDS lever: largest LDS that still reaches TargetOcc at current WG.
365 if (LDSSpecified && LDSBytes > 0 &&
366 WGLDSOcc(0, WGMin, WGMax) >= TargetOcc) {
367 uint64_t Lo = 0, Hi = LDSBytes;
368 while (Lo < Hi) {
369 uint64_t Mid = Lo + (Hi - Lo + 1) / 2;
370 if (WGLDSOcc(static_cast<uint32_t>(Mid), WGMin, WGMax) >= TargetOcc)
371 Lo = Mid;
372 else
373 Hi = Mid - 1;
374 }
375 outs() << format(Fmt: " LDS <= %llu bytes (currently %llu)\n",
376 Vals: static_cast<unsigned long long>(Lo),
377 Vals: static_cast<unsigned long long>(LDSBytes));
378 Suggested = true;
379 }
380 // Workgroup lever: largest flat workgroup size that reaches TargetOcc.
381 if (WGMax > 1 && WGLDSOcc(LDS32, 1, 1) >= TargetOcc) {
382 unsigned Lo = 1, Hi = WGMax;
383 while (Lo < Hi) {
384 unsigned Mid = Lo + (Hi - Lo + 1) / 2;
385 if (WGLDSOcc(LDS32, Mid, Mid) >= TargetOcc)
386 Lo = Mid;
387 else
388 Hi = Mid - 1;
389 }
390 if (Lo < WGMax) {
391 outs() << format(Fmt: " workgroup size <= %u (currently %u)\n", Vals: Lo,
392 Vals: WGMax);
393 Suggested = true;
394 }
395 }
396 if (!Suggested)
397 outs() << " reduce workgroup size and/or LDS\n";
398 }
399 }
400
401 if (ShowLimits) {
402 outs() << "\nPer-occupancy register limits (max regs to still reach "
403 "each level)\n";
404 outs() << format(Fmt: " %-14s %-14s %-14s\n", Vals: "Occupancy", Vals: "Max VGPRs",
405 Vals: "Max SGPRs");
406 for (unsigned Occ = MaxWaves; Occ >= 1; --Occ) {
407 unsigned MaxV =
408 AMDGPU::IsaInfo::getMaxNumVGPRs(STI, WavesPerEU: Occ, DynamicVGPRBlockSize: DynVGPRBlockSize);
409 unsigned MaxS =
410 AMDGPU::IsaInfo::getMaxNumSGPRs(STI, WavesPerEU: Occ, /*Addressable=*/true);
411 outs() << format(Fmt: " %-14u %-14u %-14u\n", Vals: Occ, Vals: MaxV, Vals: MaxS);
412 }
413 }
414
415 return 0;
416}
417