Files
TheRock/rocm-libraries/projects/hipblaslt/tensilelite/Tensile/TensileCreateLibrary/Run.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

1186 lines
48 KiB
Python

################################################################################
#
# Copyright (C) 2022-2025 Advanced Micro Devices, Inc. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
################################################################################
import rocisa
import copy
import functools
import glob
import itertools
import os
import shutil
import pickle
import zlib
from pathlib import Path
from timeit import default_timer as timer
from typing import Collection, List, NamedTuple, Optional, Union
from Tensile import SOURCE_PATH, LibraryIO
from Tensile.Common import (
CHeader,
DebugConfig,
ensurePath,
HR,
IsaVersion,
ParallelMap2,
print1,
print2,
printWarning,
printExit,
printWarning,
state,
tqdm,
setVerbosity,
getVerbosity,
)
from Tensile.Common.Architectures import gfxToIsa, isaToGfx, SUPPORTED_GFX, splitArchsFromPredicates, filterLogicFilesByPredicates
from Tensile.Common.Capabilities import makeIsaInfoMap
from Tensile.Common.GlobalParameters import assignGlobalParameters, globalParameters
from Tensile.Common.TimingInstrumentation import timing_context
from Tensile.SolutionStructs.Naming import getKernelFileBase, getKeyNoInternalArgs, getKernelNameMin
from Tensile.CustomYamlLoader import load_logic_gfx_arch
from Tensile.KernelHelperNaming import kernelObjectNameCallables, initHelperKernelObjects
from Tensile.KernelWriterAssembly import KernelWriterAssembly
from Tensile.KernelWriterBase import (
KERNEL_HELPER_FILENAME_CPP,
KERNEL_HELPER_FILENAME_H,
)
from Tensile.SolutionLibrary import MasterSolutionLibrary, PlaceholderLibrary
from Tensile.SolutionStructs import Solution
from Tensile.SolutionStructs.Solution import (
raiseIfTypeMismatches,
mergeTypeMismatchCollector,
resetTypeMismatchCollector,
)
from Tensile.verify_stinky_comment_vs_elf_text import verify_stinky_paths
from Tensile.Toolchain.Assembly import makeAssemblyToolchain, buildAssemblyCodeObjectFiles
from Tensile.Toolchain.Source import makeSourceToolchain, buildSourceCodeObjectFiles
from Tensile.Toolchain.Validators import (
ToolchainDefaults,
validateToolchain,
)
from Tensile.Toolchain.Component import Assembler
from Tensile.Utilities.Decorators.Profile import profile
from Tensile.Utilities.Decorators.Timing import timing
from .ParseArguments import parseArguments
def libraryRoot(outputPath: Union[str, Path]) -> Path:
"""The library/ root directory under outputPath.
Used as the dispatch root by builders that fan kernels out into per-base
subdirectories at write time.
"""
return Path(outputPath) / "library"
def libraryDir(outputPath: Union[str, Path], arch: str) -> Path:
"""The per-base-arch library subdirectory: <outputPath>/library/<base>/.
Target features (xnack+/xnack-, sramecc, etc.) are stripped from the path —
variants of one base co-locate in one directory, disambiguated by kernel
filename suffix. Layout matches the runtime probe in tensile_host.cpp which
strips at the first colon before looking up the subdirectory.
"""
return libraryRoot(outputPath) / arch.split(":")[0]
def _baseArchs(archs: Collection[str]) -> List[str]:
"""Unique base archs (xnack/sramecc stripped), sorted for determinism."""
return sorted({a.split(":")[0] for a in archs})
def tensileLibraryFile(outputPath: Union[str, Path], arch: str, library_format: str = "msgpack") -> Path:
"""The canonical TensileLibrary path for one base arch under outputPath.
Composes ``<outputPath>/library/<base>/TensileLibrary.<ext>`` where ``ext``
is ``.yaml`` for the YAML format and ``.dat`` for msgpack. The base arch
is derived from ``arch`` via the same colon-strip rule as ``libraryDir``,
so cooked variants like ``gfx942:sramecc+:xnack+`` resolve to the same
file as the bare ``gfx942`` arch.
This is the file that ``writeClientConfigIni``'s ``libraryFile`` argument
must point to under the per-base layout. Callers (BenchmarkProblems'
cache-hit branch, ClientWriter's benchmark-parameters helper) reach for
it from different parts of the pipeline; the helper keeps the
"library/<base>/TensileLibrary.<ext>" naming convention in one place so
future format/extension changes touch a single call site.
"""
ext = ".yaml" if library_format == "yaml" else ".dat"
return libraryDir(outputPath, arch) / f"TensileLibrary{ext}"
class KernelCodeGenResult(NamedTuple):
err: int
src: Union[str, bytes]
header: Optional[str]
name: str
targetObjFilename: str
isa: IsaVersion
wavefrontSize: int
cuoccupancy: int
pgr: int
mathclk: int
class KernelMinResult(NamedTuple):
err: int
cuoccupancy: int
pgr: int
mathclk: int
def _stinky_asm_verify_wanted(isa: IsaVersion) -> bool:
"""Return True if asm/.o Stinky size check should run for this kernel.
Requires ``CheckASMCodeSize`` and gfx1250. When True, callers should avoid joblib for
write+assemble so logs stay on one process.
"""
return bool(globalParameters["CheckASMCodeSize"]) and isaToGfx(isa) == "gfx1250"
def _stinky_out(msg: str) -> None:
"""Emit one user-visible log line for Stinky verify.
Writes to stderr via ``os.write(2, ...)``. Under pytest-xdist, worker stdout may be
hidden; stderr often still appears in the terminal.
"""
try:
os.write(2, (msg + "\n").encode("utf-8", errors="replace"))
except OSError:
pass
def _verify_stinky_asm_comment_vs_elf_text(s_path: Path, o_path: Path, kernel_base: str) -> None:
"""After assembling ``s_path`` → ``o_path``, verify Stinky vs ELF ``.text``.
Call only when ``_stinky_asm_verify_wanted(isa)`` is True. Uses ``verify_stinky_comment_vs_elf_text``
(``readelf`` / ``llvm-readelf``; ``ROCM_PATH``, ``LLVM_BIN``, or ``PATH``). Forwards messages
through ``_stinky_out``. Exits via ``printExit`` on mismatch (1) or tool/readelf error (2).
Args:
s_path: Path to the generated ``.s`` file.
o_path: Path to the assembled ``.o`` file.
kernel_base: Short name for messages (usually the asm stem).
"""
_stinky_out(f"CheckASMCodeSize: running verify for {kernel_base}")
try:
code, out_s, err_s = verify_stinky_paths(s_path, o_path)
except Exception as ex:
printExit(f"CheckASMCodeSize: could not run verifier for {kernel_base}: {ex}")
if out_s:
for line in out_s.splitlines():
_stinky_out(line)
if err_s:
for line in err_s.splitlines():
_stinky_out(line)
if code == 2:
printExit(f"CheckASMCodeSize: verifier error for {kernel_base}")
if code == 1:
printExit(
f"CheckASMCodeSize: STINKY_TOTAL_INST_BYTES vs ELF .text mismatch for {kernel_base}"
)
if code == 0:
out = (out_s or "") + (err_s or "")
matched = "OK STINKY" in out
if matched:
_stinky_out(
f"CheckASMCodeSize: OK STINKY_TOTAL_INST_BYTES vs ELF .text match for {kernel_base}"
)
def memCompress(obj):
return zlib.compress(pickle.dumps(obj))
def memDecompress(byt):
return pickle.loads(zlib.decompress(byt))
def processKernelSource(kernelWriterAssembly, data, outOptions, splitGSU, kernel, compress = False) -> KernelCodeGenResult:
"""
Generate source for a single kernel.
Returns (error, source, header, kernelName).
"""
kernelWriter = kernelWriterAssembly
kernelWriter.setRocIsa(data, outOptions)
asmFilename = getKernelFileBase(splitGSU, kernel)
err, src = kernelWriter.getSourceFileString(kernel)
if compress:
src = memCompress(src)
header = kernelWriter.getHeaderFileString(kernel)
objFilename = kernel._state.get("codeObjectFile", None)
pgr = int(kernel["PrefetchGlobalRead"])
cuocc = kernel["CUOccupancy"]
if cuocc <= 0 and getVerbosity() >= 2:
print2(
f"[codegen] CUOccupancy={cuocc} (<=0) after codegen for kernel {asmFilename}; "
f"runtime will clamp to 1."
)
return KernelCodeGenResult(
err, src, header, asmFilename, objFilename, tuple(kernel["ISA"]), \
kernel["WavefrontSize"], cuocc, \
pgr, kernel["MathClocksUnrolledLoop"]
)
def _checkInvalidSolutionsAndKernels(errorTolerant, result, kernel):
if result.err != 0:
if not errorTolerant:
print(
"\nKernel generation failed for kernel: {}".format(
kernel["SolutionIndex"]
)
)
print(kernel["SolutionNameMin"])
return True
return False
def _checkInvalidSolutions(splitGSU, removeKernelNames, solutions):
invalids = []
for solution in solutions:
solutionKernels = solution.getKernels()
for kernel in solutionKernels:
kName = getKeyNoInternalArgs(kernel, splitGSU)
if kName in removeKernelNames:
invalids.append(True)
break
invalids.append(False)
return invalids
def removeInvalidSolutionsAndKernels(results, kernels, solutions, errorTolerant, printLevel: bool, splitGSU: bool):
removeKernelsAndResultsFlag = ParallelMap2(functools.partial(_checkInvalidSolutionsAndKernels, errorTolerant),
zip(results, kernels), "check invalid kernels and results", return_as="list")
if any(removeKernelsAndResultsFlag) and not errorTolerant:
printExit("** kernel generation failure **")
removeKernelNames = {getKeyNoInternalArgs(kernel, splitGSU) for invalid, kernel in zip(removeKernelsAndResultsFlag, kernels) if invalid}
kernels[:] = [kernel for invalid, kernel in zip(removeKernelsAndResultsFlag, kernels) if not invalid]
removeSolutionsFlag = []
for solution in (
tqdm(solutions, "Finding invalid solutions")
if printLevel > 1
else solutions
):
solutionKernels = solution.getKernels()
flag = False
for kernel in solutionKernels:
kName = getKeyNoInternalArgs(kernel, splitGSU)
if kName in removeKernelNames:
flag = True
break
removeSolutionsFlag.append(flag)
solutions[:] = [solut for invalid, solut in zip(removeSolutionsFlag, solutions) if not invalid]
results[:] = [rel for invalid, rel in zip(removeKernelsAndResultsFlag, results) if not invalid]
def passPostKernelInfoToSolution(results, kernels, solutions, splitGSU: bool):
resultDict = {}
for kernIdx, r in enumerate(results):
kName = getKernelNameMin(kernels[kernIdx], splitGSU)
resultDict["%s"%kName] = r
for solution in solutions:
solutionKernels = solution.getKernels()
for kernel in solutionKernels:
kName = getKernelNameMin(kernel, splitGSU)
result = resultDict["%s"%kName]
solution._state["CUOccupancy"] = result.cuoccupancy
solution._state["PrefetchGlobalRead"] = result.pgr
solution._state["MathClocksUnrolledLoop"] = result.mathclk
def passPostKernelInfoToLibrary(results, kernels, masterLibraries, splitGSU: bool):
resultDict = {}
for kernIdx, r in enumerate(results):
kName = getKernelFileBase(splitGSU, kernels[kernIdx])
resultDict["%s"%kName] = r
for archName, masterLibrary in masterLibraries.items():
for solIdx, sol in masterLibrary.solutions.items():
solutionKernels = sol.originalSolution.getKernels()
for kernel in solutionKernels:
kName = getKernelFileBase(splitGSU, kernel)
try:
result = resultDict["%s"%kName]
sol.sizeMapping.CUOccupancy = result.cuoccupancy
sol.sizeMapping.MathClocksUnrolledLoop = result.mathclk
sol.sizeMapping.PrefetchGlobalRead = sol.originalSolution._state['PrefetchGlobalRead']
sol.sizeMapping.NonTemporalA = sol.originalSolution._state['NonTemporalA']
sol.sizeMapping.NonTemporalB = sol.originalSolution._state['NonTemporalB']
sol.sizeMapping.adaptiveGemmNTAB = sol.originalSolution._state.get('AdaptiveGemmNTAB', 0)
sol.sizeMapping.NonTemporalD = sol.originalSolution._state['NonTemporalD']
sol.sizeMapping.WaveSeparateGlobalReadA = sol.originalSolution._state['WaveSeparateGlobalReadA']
sol.sizeMapping.WaveSeparateGlobalReadB = sol.originalSolution._state['WaveSeparateGlobalReadB']
sol.sizeMapping.UnrollLoopSwapGlobalReadOrder = sol.originalSolution._state['UnrollLoopSwapGlobalReadOrder']
sol.sizeMapping.DirectToVgprA = bool(sol.originalSolution._state['DirectToVgprA'])
sol.sizeMapping.DirectToVgprB = bool(sol.originalSolution._state['DirectToVgprB'])
except KeyError:
print(f"\n{'='*80}")
print(f"ERROR: KeyError in masterLibrary.solutions")
print(f"Architecture: {archName}")
print(f"Solution Index: {solIdx}")
print(f"Solution source file: {getattr(sol, 'srcName', 'Unknown')}")
print(f"Solution library logic index: {getattr(sol, 'libraryLogicIndex', 'Unknown')}")
print(f"Missing kernel name: {kName}")
print(f"{'='*80}\n")
raise
masterLibrary.lazyLibraries = dict(sorted(masterLibrary.lazyLibraries.items()))
for name, lib in masterLibrary.lazyLibraries.items():
for solIdx, sol in lib.solutions.items():
solutionKernels = sol.originalSolution.getKernels()
for kernel in solutionKernels:
kName = getKernelFileBase(splitGSU, kernel)
try:
result = resultDict["%s"%kName]
sol.sizeMapping.CUOccupancy = result.cuoccupancy
sol.sizeMapping.MathClocksUnrolledLoop = result.mathclk
sol.sizeMapping.PrefetchGlobalRead = sol.originalSolution._state['PrefetchGlobalRead']
sol.sizeMapping.NonTemporalA = sol.originalSolution._state['NonTemporalA']
sol.sizeMapping.NonTemporalB = sol.originalSolution._state['NonTemporalB']
sol.sizeMapping.adaptiveGemmNTAB = sol.originalSolution._state.get('AdaptiveGemmNTAB', 0)
sol.sizeMapping.NonTemporalD = sol.originalSolution._state['NonTemporalD']
sol.sizeMapping.WaveSeparateGlobalReadA = sol.originalSolution._state['WaveSeparateGlobalReadA']
sol.sizeMapping.WaveSeparateGlobalReadB = sol.originalSolution._state['WaveSeparateGlobalReadB']
sol.sizeMapping.UnrollLoopSwapGlobalReadOrder = sol.originalSolution._state['UnrollLoopSwapGlobalReadOrder']
sol.sizeMapping.DirectToVgprA = bool(sol.originalSolution._state['DirectToVgprA'])
sol.sizeMapping.DirectToVgprB = bool(sol.originalSolution._state['DirectToVgprB'])
except KeyError:
print(f"\n{'='*80}")
print(f"ERROR: KeyError in lazyLibrary")
print(f"Architecture: {archName}")
print(f"LazyLibrary name: {name}")
print(f"Solution Index: {solIdx}")
print(f"Solution source file: {getattr(sol, 'srcName', 'Unknown')}")
print(f"Solution library logic index: {getattr(sol, 'libraryLogicIndex', 'Unknown')}")
print(f"Missing kernel name: {kName}")
print(f"Total kernels in this solution: {len(solutionKernels)}")
print(f"{'='*80}\n")
raise
def writeAssembly(asmPath: Union[Path, str], result: KernelCodeGenResult):
if result.err:
printExit(f"Failed to build kernel {result.name} because it has error code {result.err}")
path = Path(asmPath) / f"{result.name}.s"
isa = result.isa
wfsize = result.wavefrontSize
with open(path, "w", encoding="utf-8") as f:
src = result.src
if isinstance(src, bytes):
src = memDecompress(src)
f.write(src)
minResult = KernelMinResult(result.err, result.cuoccupancy, result.pgr, result.mathclk)
return path, isa, wfsize, minResult
def writeHelpers(
outputPath, kernelHelperObjs, KERNEL_HELPER_FILENAME_CPP, KERNEL_HELPER_FILENAME_H
):
kernelSourceFilename = os.path.join(os.path.normcase(outputPath), KERNEL_HELPER_FILENAME_CPP)
kernelHeaderFilename = os.path.join(os.path.normcase(outputPath), KERNEL_HELPER_FILENAME_H)
with open(kernelHeaderFilename, "w", encoding="utf-8") as kernelHeaderFile, open(
kernelSourceFilename, "w", encoding="utf-8"
) as kernelSourceFile:
kernelSourceFile.write(CHeader)
kernelHeaderFile.write(CHeader)
kernelSourceFile.write('#include "Kernels.h"\n')
kernelHeaderFile.write("#pragma once\n")
kernelHeaderFile.write("#include <hip/hip_runtime.h>\n")
kernelHeaderFile.write("#include <hip/hip_ext.h>\n\n")
kernelHeaderFile.write('#include "KernelHeader.h"\n\n')
HeaderText = ""
for ko in kernelHelperObjs:
kernelName = ko.getKernelName()
(err, src) = ko.getSourceFileString()
kernelSourceFile.write(src)
if err:
print("*** warning: invalid kernel#%u" % kernelName)
HeaderText += ko.getHeaderFileString()
kernelHeaderFile.write(HeaderText)
def writeSolutionsAndKernels(
outputPath,
asmToolchain,
srcToolchain,
solutions,
kernels,
kernelHelperObjs,
kernelWriterAssembly,
splitGSU: bool,
cmdlineArchs: List[str],
disableAsmComments: bool=False,
errorTolerant: bool=False,
generateSourcesAndExit: bool=False,
compress: bool=True,
removeTemporaries: bool=True,
):
if globalParameters["PythonProfile"]:
globalParameters["CpuThreads"] = 0
printWarning("Python profiling is enabled. CpuThreads set to 0.")
import yappi
yappi.start()
codeObjectFiles = []
with timing_context("python_kernel_setup"):
outputPath = Path(outputPath)
# Builders fan out into <destRoot>/<base-arch>/ at the moment of write.
# Pre-create the per-base subdirs so concurrent emit doesn't race mkdir.
destRoot = ensurePath(libraryRoot(outputPath))
for base in _baseArchs(cmdlineArchs):
ensurePath(libraryDir(outputPath, base))
buildTmpPath = ensurePath(outputPath / "build_tmp" / outputPath.stem.upper()) #
assemblyTmpPath = ensurePath(
buildTmpPath / "assembly"
) # Temp path for generated assembly files (.s)
objectTmpPath = ensurePath(
buildTmpPath / "code_object_tmp"
) # Temp path for HSA code object files (.hsaco)
asmKernels = [k for k in kernels if k["KernelLanguage"] == "Assembly"]
visited = set()
duplicates = 0
for k in asmKernels:
base = getKernelFileBase(splitGSU, k)
k.duplicate = True if base in visited else False
if not k.duplicate:
k["BaseName"] = base
duplicates += k.duplicate
print2(f"Duplicate: {base}")
visited.add(base)
print1(f"Number of duplicate kernels: {duplicates}")
outOptions = rocisa.rocIsa.getInstance().getOutputOptions()
outOptions.outputNoComment = disableAsmComments
numAsmKernels = len(asmKernels)
numKernels = len(asmKernels)
assert numKernels == numAsmKernels, "Only assembly kernels are supported in TensileLite"
asmIter = zip(
itertools.repeat(kernelWriterAssembly),
itertools.repeat(rocisa.rocIsa.getInstance().getData()),
itertools.repeat(outOptions),
itertools.repeat(splitGSU),
asmKernels
)
memcompress = numAsmKernels > 10000
with timing_context("python_kernel_codegen"):
asmResults = ParallelMap2(functools.partial(processKernelSource, compress=memcompress), asmIter, "Generating assembly kernels", return_as="list")
with timing_context("python_kernel_validate"):
removeInvalidSolutionsAndKernels(
asmResults, asmKernels, solutions, errorTolerant, getVerbosity(), splitGSU
)
passPostKernelInfoToSolution(
asmResults, asmKernels, solutions, splitGSU
)
def assemble(ret):
p, isa, wavefrontsize, _ = ret
o_path = p.with_suffix(".o")
try:
asmToolchain.assembler(isaToGfx(isa), wavefrontsize, str(p), str(o_path))
except RuntimeError as e:
printWarning(f"Failed to assemble {p}: {e}")
return
if _stinky_asm_verify_wanted(isa):
_verify_stinky_asm_comment_vs_elf_text(p, o_path, p.stem)
if removeTemporaries:
p.unlink()
unaryWriteAssembly = functools.partial(writeAssembly, assemblyTmpPath)
compose = lambda *F: functools.reduce(lambda f, g: lambda x: f(g(x)), F)
with timing_context("python_kernel_write_assemble"):
ret = ParallelMap2(
compose(assemble, unaryWriteAssembly),
asmResults,
"Writing assembly kernels",
return_as="list",
multiArg=False,
)
# Remove solutions whose kernels failed to assemble (no .o file produced)
failedBases = {k["BaseName"] for k in asmKernels
if not k.duplicate and not (assemblyTmpPath / (k["BaseName"] + ".o")).exists()}
if failedBases:
solutions[:] = [s for s in solutions
if getKernelFileBase(splitGSU, s.getKernels()[0]) not in failedBases]
asmKernels[:] = [k for k in asmKernels if k.get("BaseName", None) not in failedBases]
with timing_context("python_kernel_write_helpers"):
writeHelpers(outputPath, kernelHelperObjs, KERNEL_HELPER_FILENAME_CPP, KERNEL_HELPER_FILENAME_H)
srcKernelFile = Path(outputPath) / "Kernels.cpp"
if globalParameters["PythonProfile"]:
yappi.stop()
yappi.get_func_stats().save("yappi_results.profile", type="callgrind")
with open("yappi_results.txt", "w") as f:
yappi.get_func_stats().print_all(out=f)
if globalParameters["CpuThreads"] != 0:
with open("yappi_thread_stats.txt", "w") as f:
yappi.get_thread_stats().print_all(out=f)
if not generateSourcesAndExit:
with timing_context("python_kernel_build_co"):
codeObjectFiles += buildAssemblyCodeObjectFiles(
asmToolchain.linker,
asmToolchain.bundler,
[k for k in asmKernels if not k.duplicate],
destRoot,
assemblyTmpPath,
compress,
)
with timing_context("python_kernel_build_src_co"):
buildSourceCodeObjectFiles(
srcToolchain.compiler,
srcToolchain.bundler,
destRoot,
objectTmpPath,
outputPath,
srcKernelFile,
cmdlineArchs,
)
if removeTemporaries and not generateSourcesAndExit:
buildTmp = outputPath / "build_tmp"
if buildTmp.exists() and buildTmp.is_dir():
shutil.rmtree(buildTmp)
return codeObjectFiles, numKernels
def writeSolutionsAndKernelsTCL(
outputPath,
asmToolchain,
srcToolchain,
solutions,
kernels,
kernelHelperObjs,
kernelWriterAssembly,
cmdlineArchs: List[str],
disableAsmComments: bool=False,
compress: bool=True,
removeTemporaries: bool=True
):
outputPath = Path(outputPath)
# Builders fan out into <destRoot>/<base-arch>/ at the moment of write.
# Pre-create the per-base subdirs so concurrent emit doesn't race mkdir.
destRoot = ensurePath(libraryRoot(outputPath))
for base in _baseArchs(cmdlineArchs):
ensurePath(libraryDir(outputPath, base))
buildTmpPath = ensurePath(outputPath / "build_tmp" / outputPath.stem.upper())
assemblyTmpPath = ensurePath(
buildTmpPath / "assembly"
) # Temp path for generated assembly files (.s)
objectTmpPath = ensurePath(
buildTmpPath / "code_object_tmp"
) # Temp path for HSA code object files (.hsaco)
asmKernels = [k for k in kernels if k["KernelLanguage"] == "Assembly"]
visited = set()
duplicates = 0
splitGSU = False
for k in asmKernels:
base = getKernelFileBase(splitGSU, k)
k["BaseName"] = base
k.duplicate = True if base in visited else False
duplicates += k.duplicate
print2(f"Duplicate: {base}")
visited.add(base)
print1(f"Number of duplicate kernels: {duplicates}")
uniqueAsmKernels = [k for k in asmKernels if not k.duplicate]
def assemble(ret, removeTemporaries: bool):
asmPath, isa, wavefrontsize, result = ret
o_path = asmPath.with_suffix(".o")
asmToolchain.assembler(isaToGfx(isa), wavefrontsize, str(asmPath), str(o_path))
if _stinky_asm_verify_wanted(isa):
_verify_stinky_asm_comment_vs_elf_text(asmPath, o_path, asmPath.stem)
if removeTemporaries:
asmPath.unlink()
return result
unaryAssemble = functools.partial(assemble, removeTemporaries=removeTemporaries)
outOptions = rocisa.rocIsa.getInstance().getOutputOptions()
outOptions.outputNoComment = not disableAsmComments
memcompress = len(uniqueAsmKernels) > 10000
unaryProcessKernelSource = functools.partial(
processKernelSource,
kernelWriterAssembly,
rocisa.rocIsa.getInstance().getData(),
outOptions,
splitGSU,
compress = memcompress,
)
unaryWriteAssembly = functools.partial(writeAssembly, assemblyTmpPath)
def compose(assemble, unaryWriteAssembly, unaryProcessKernelSource):
def composed_function(kernel):
processed_kernel = unaryProcessKernelSource(kernel)
written_kernel = unaryWriteAssembly(processed_kernel)
assembled_kernel = assemble(written_kernel)
return processed_kernel
return composed_function
results = ParallelMap2(
compose(unaryAssemble, unaryWriteAssembly, unaryProcessKernelSource),
uniqueAsmKernels,
"Generating assembly kernels",
multiArg=False,
return_as="list"
)
buildAssemblyCodeObjectFiles(
asmToolchain.linker,
asmToolchain.bundler,
asmKernels,
destRoot,
assemblyTmpPath,
compress,
)
writeHelpers(outputPath, kernelHelperObjs, KERNEL_HELPER_FILENAME_CPP, KERNEL_HELPER_FILENAME_H)
srcKernelFile = Path(outputPath) / "Kernels.cpp"
buildSourceCodeObjectFiles(
srcToolchain.compiler,
srcToolchain.bundler,
destRoot,
objectTmpPath,
outputPath,
srcKernelFile,
cmdlineArchs,
)
return len(uniqueAsmKernels), uniqueAsmKernels, results
@timing
def copyStaticFiles(outputPath):
libraryStaticFiles = [
"TensileTypes.h",
"tensile_bfloat16.h",
"tensile_float8_bfloat8.h",
"KernelHeader.h",
"ReductionTemplate.h",
"memory_gfx.h",
]
for fileName in libraryStaticFiles:
shutil.copy(os.path.join(SOURCE_PATH, fileName), outputPath)
return libraryStaticFiles
@timing
def generateKernelObjectsFromSolutions(solutions):
kernels = []
kernelNames = set()
for solution in solutions:
solutionKernels = solution.getKernels()
for kernel in solutionKernels:
kName = getKeyNoInternalArgs(kernel, False)
if kName not in kernelNames:
kernels.append(kernel)
kernelNames.add(kName)
return kernels
def generateKernelHelperObjects(solutions: List[Solution], cxxCompiler: str, isaInfoMap):
"""
Generates a unique list of kernel helpers.
Kernel helpers are used to generate hip source code kernels called
before/after gemm kernels. This function creates a minimal list of
kernel helpers required to support the solutions reuested in a build.
The list of kernel helpers is then used to write Kernels.cpp/h to
disk. To ensure the ActivationEnumHeaders are written first, the
list is sorted such that those kernel helpers appear first.
Args:
solutions: a list of solutions to process.
cxxCompiler: the full path to the cxxCompiler.
Returns:
List of kernel helpers.
"""
khos = []
visited = set()
for solution in solutions:
for kernelHelperType, callable in kernelObjectNameCallables():
buildMask = []
names = callable(solution)
if names:
sortByEnum = lambda x: ("Enum" in x, names.index(x))
names = sorted(names, key=sortByEnum, reverse=True)
for name in names:
if name not in visited:
visited.add(name)
buildMask.append(True)
else:
buildMask.append(False)
if any(buildMask):
kho = initHelperKernelObjects(solution, kernelHelperType, cxxCompiler, isaInfoMap)
kho = list(itertools.compress(kho, buildMask))
if kho:
khos.extend(kho)
khos = list(set(khos))
sortByEnum = lambda x: ("Enum" in x.getKernelName(), khos.index(x))
return sorted(khos, key=sortByEnum, reverse=True) # Ensure that we write Enum kernel helpers are first in list
def _renameFallbackPlaceholders(node, arch: str) -> None:
"""Walk a library tree, appending "_<arch>" to fallback PlaceholderLibrary names.
Mutates `filenamePrefix` on every PlaceholderLibrary leaf whose existing
prefix already encodes a fallback (i.e. came from the merged-in fallback
master library and therefore ends with "_fallback") and which has not yet
been arch-suffixed. Idempotent: a prefix already ending in "_<arch>"
is left alone so a second pass cannot double-suffix.
"""
if node is None:
return
if isinstance(node, PlaceholderLibrary):
if "_fallback" in node.filenamePrefix and not node.filenamePrefix.endswith("_" + arch):
node.filenamePrefix = node.filenamePrefix + "_" + arch
return
rows = getattr(node, "rows", None)
if rows:
for row in rows:
_renameFallbackPlaceholders(row.get("library"), arch)
mapping = getattr(node, "mapping", None)
if mapping:
for child in mapping.values():
_renameFallbackPlaceholders(child, arch)
def renameFallbacksPerArch(masterLibraries) -> None:
"""Make merged-in fallback lazy-library filenames arch-specific.
`MasterSolutionLibrary.merge` aliases the same fallback lazy library across
every per-arch master that absorbs it (keys collide on the un-suffixed
"_fallback" name). That alias means the per-arch *_fallback.dat files are
written with overlapping filenames carrying different solution-index spaces,
and the per-arch Mapping write loop's `name.endswith("_<arch>")` filter drops
every fallback entry — runtime then can't resolve fallback-served dtypes.
Per-arch deep-copy here splits the alias and arch-suffixes both the
`lazyLibraries` dict keys and the matching PlaceholderLibrary nodes inside
the master library tree, so:
- on-disk filenames diverge (no overlay collision),
- the per-arch Mapping filter matches "_fallback_<arch>" naturally, and
- each arch keeps its own re-indexed copy of the fallback solutions.
"""
for arch in list(masterLibraries.keys()):
master = copy.deepcopy(masterLibraries[arch])
masterLibraries[arch] = master
renamed = {}
for name, lib in master.lazyLibraries.items():
if "_fallback" in name and not name.endswith("_" + arch):
renamed[name + "_" + arch] = lib
else:
renamed[name] = lib
master.lazyLibraries = renamed
_renameFallbackPlaceholders(master.library, arch)
@timing
def generateLogicDataAndSolutions(logicFiles, args, assembler: Assembler, isaInfoMap):
if ";" in args["Architecture"]:
archs = args["Architecture"].split(";") # user arg list format
else:
archs = args["Architecture"].split("_") # workaround for cmake list in list issue
solutions = []
masterLibraries = {}
nextSolIndex = 0
splitGSU = False
printSolutionRejectionReason = True
printIndexAssignmentInfo = False
fIter = zip(
logicFiles,
itertools.repeat(assembler),
itertools.repeat(splitGSU),
itertools.repeat(printSolutionRejectionReason),
itertools.repeat(printIndexAssignmentInfo),
itertools.repeat(isaInfoMap),
itertools.repeat(args["LazyLibraryLoading"]),
)
def libraryIter(lib: MasterSolutionLibrary):
if len(lib.solutions):
for i, s in enumerate(lib.solutions.items()):
yield (i, *s)
else:
for _, lazyLib in lib.lazyLibraries.items():
yield from libraryIter(lazyLib)
def mergeTypeMismatchSnapshot(
aggregate: dict,
snapshot: dict,
) -> None:
"""Merge one parseLibraryLogicFile() mismatch snapshot into aggregate."""
for key, entry in snapshot.items():
target = aggregate.setdefault(key, {"count": 0, "values": set(), "files": set()})
target["count"] += entry["count"]
target["values"].update(entry["values"])
target["files"].update(entry["files"])
# parseLibraryLogicData() uses Solution.py's module-level type mismatch
# collector as a per-file scratch buffer: it resets the collector at parse
# start, builds the ProblemType/Solution objects, and returns a snapshot in
# the LibraryLogic tuple. ParallelMap2 can execute those parses in worker
# processes or in this process (notably when CpuThreads resolves to 1). In
# the in-process path, the scratch buffer is the same global collector this
# function can see, and after parsing it still contains the most recently
# parsed file's mismatches.
#
# Keep the run-level aggregate detached from that global collector while
# consuming parse results. Merging returned snapshots directly into the live
# collector would mix aggregate state with parser scratch state: a later
# parse can reset previously merged aggregate state, and the final file's
# snapshot can be double-counted. Once every logic file has been parsed,
# replace the global collector with this clean aggregate so
# raiseIfTypeMismatches() can format the existing fatal aggregate error.
typeMismatchAggregate: dict = {}
parsedLibraries = ParallelMap2(
LibraryIO.parseLibraryLogicFile, fIter, "Loading Logics...", return_as="generator"
)
for library in parsedLibraries:
_, architectureName, _, _, _, newLibrary, typeMismatches = library
mergeTypeMismatchSnapshot(typeMismatchAggregate, typeMismatches)
if architectureName == "":
continue
if architectureName in masterLibraries:
nextSolIndex = masterLibraries[architectureName].merge(newLibrary, nextSolIndex)
else:
masterLibraries[architectureName] = newLibrary
masterLibraries[architectureName].version = args["CodeObjectVersion"]
# After all YAML files have been parsed and Solution objects created,
# fail on any type mismatches that were collected.
resetTypeMismatchCollector()
mergeTypeMismatchCollector(typeMismatchAggregate)
raiseIfTypeMismatches()
# Sort masterLibraries to make global soln index values deterministic
solnReIndex = 0
masterLibraries = dict(sorted(masterLibraries.items()))
for _, masterLibrary in masterLibraries.items():
for _, sol in masterLibrary.solutions.items():
sol.index = solnReIndex
solnReIndex += 1
# Sort masterLibrary to make global soln index values deterministic
masterLibrary.lazyLibraries = dict(sorted(masterLibrary.lazyLibraries.items()))
for name, lib in masterLibrary.lazyLibraries.items():
# Sort solns by the lib logic file they were generated from
lib.solutions = {
k: lib.solutions[k]
for k in sorted(lib.solutions, key=lambda idx: lib.solutions[idx].srcName)
}
for _, sol in lib.solutions.items():
sol.index = solnReIndex
solnReIndex += 1
if args["GenSolTable"]:
matchTable = {}
# Match yaml file solutions to solution index
for _, masterLibrary in masterLibraries.items():
for _, _, s in libraryIter(masterLibrary):
matchTable[s.index] = [s.srcName, s.libraryLogicIndex]
LibraryIO.write("MatchTable", matchTable)
fallbackAdded = "fallback" in masterLibraries.keys()
if fallbackAdded:
for key, value in masterLibraries.items():
if key != "fallback":
value.merge(masterLibraries["fallback"])
masterLibraries.pop("fallback")
if fallbackAdded:
# Must run AFTER merge (so per-arch masters carry their own fallback
# entries) and BEFORE the codeObjectFile-assignment loop below (which
# snapshots the dict key as the on-disk filename for each solution).
renameFallbacksPerArch(masterLibraries)
solIndex = []
for _, masterLibrary in masterLibraries.items():
for _, sol in masterLibrary.solutions.items():
solutions.append(sol.originalSolution)
solIndex.append(sol.index)
for name, lib in masterLibrary.lazyLibraries.items():
for _, sol in lib.solutions.items():
sol.originalSolution._state["codeObjectFile"] = name
solutions.append(sol.originalSolution)
solIndex.append(sol.index)
# Get the solution index and it's codeObjectFile name
codeObjectFilesIndex = {}
for solution, index in zip(solutions, solIndex):
if "codeObjectFile" in solution._state and solution._state["codeObjectFile"] is not None:
if solution._state["codeObjectFile"] in codeObjectFilesIndex:
codeObjectFilesIndex[solution._state["codeObjectFile"]] = min(index, codeObjectFilesIndex[solution._state["codeObjectFile"]])
else:
codeObjectFilesIndex[solution._state["codeObjectFile"]] = index
# Reorder to int: name format
codeObjectFilesIndex = {v: k for k, v in codeObjectFilesIndex.items()}
# Reorder to maintain ascending order by index
codeObjectFilesIndex = dict(sorted(codeObjectFilesIndex.items()))
# remove duplicates while preserving order
numSoln = len(solutions)
solutions = dict.fromkeys(solutions).keys()
print1(f"Number of solutions parsed: {numSoln}")
print1(f"Number of unique solutions: {len(solutions)}")
return solutions, masterLibraries, codeObjectFilesIndex
################################################################################
# Tensile Create Library
################################################################################
@profile
def run():
start = timer()
print1("")
print1(HR)
print1("# Tensile Create Library")
print2(HR)
print2("")
arguments = parseArguments()
setVerbosity(arguments["PrintLevel"])
outputPath = Path(ensurePath(os.path.abspath(arguments["OutputPath"])))
cxxCompiler, _, offloadBundler, _, _ = validateToolchain(
arguments["CxxCompiler"],
arguments["CCompiler"],
arguments["OffloadBundler"],
arguments["Assembler"],
ToolchainDefaults.HIP_CONFIG,
)
if ";" in arguments["Architecture"]:
archs = arguments["Architecture"].split(";")
else:
archs = arguments["Architecture"].split("_")
archs = SUPPORTED_GFX if "all" in archs else archs
archs, requestedPredicateMap = splitArchsFromPredicates(archs)
targetIsas = [gfxToIsa(a) for a in archs]
isaInfoMap = makeIsaInfoMap(targetIsas, cxxCompiler)
assignGlobalParameters(arguments, isaInfoMap)
asmToolchain = makeAssemblyToolchain(
cxxCompiler,
offloadBundler,
arguments["CodeObjectVersion"],
arguments["BuildIdKind"],
arguments["AsmDebug"],
)
srcToolchain = makeSourceToolchain(
cxxCompiler,
offloadBundler,
arguments["AsanBuild"],
arguments["BuildIdKind"],
save_temps=False
)
print1(asmToolchain.assembler)
print1(asmToolchain.bundler)
if not os.path.exists(arguments["LogicPath"]):
printExit(f"LogicPath {arguments['LogicPath']} doesn't exist")
logicExtFormat = ".yaml"
if arguments["LogicFormat"] == "yaml":
pass
elif arguments["LogicFormat"] == "json":
logicExtFormat = ".json"
else:
printExit(f"Unrecognized LogicFormat: {arguments['LogicFormat']}")
def archMatch(arch: str, archs: List[str]):
return (arch in archs) or any(a.startswith(arch) for a in archs)
def validLogicFile(p: Path):
return p.suffix == logicExtFormat and (
"all" in archs or archMatch(load_logic_gfx_arch(p), archs)
)
globPattern = os.path.join(
arguments["LogicPath"], f"**/{arguments['LogicFilter']}{logicExtFormat}"
)
print1(f"# LogicFilter: {globPattern}")
logicFiles = [
file for file in glob.iglob(globPattern, recursive=True)
]
logicFiles = [file for file in logicFiles if validLogicFile(Path(file))]
print1(f"# Experimental: {arguments['Experimental']}")
if not arguments["Experimental"]:
logicFiles = [
file for file in logicFiles if "experimental" not in map(str.lower, Path(file).parts)
]
print1("# Archs: " + ' ,'.join(archs))
if requestedPredicateMap:
print1("# Predicates:\n" + "\n".join(f"# {arch}: {', '.join(v) if v else 'all variants'}" for arch, v in requestedPredicateMap.items()))
numPrior = len(logicFiles)
logicFiles = filterLogicFilesByPredicates(logicFiles, requestedPredicateMap)
print1(f"# Filtered {numPrior - len(logicFiles)} logic files not matching requested predicates")
print1(f"# LibraryLogicFiles: {len(logicFiles)}")
for logicFile in logicFiles:
print2("# %s" % logicFile)
start_glds = timer()
solutions, masterLibraries, libraryMapping = generateLogicDataAndSolutions(
logicFiles, arguments, asmToolchain.assembler, isaInfoMap
)
stop_glds = timer()
print(f"Time to load yaml files (s): {(stop_glds-start_glds):3.2f}")
kernels = generateKernelObjectsFromSolutions(solutions)
kernelHelperObjs = generateKernelHelperObjects(kernels, str(asmToolchain.assembler.path), isaInfoMap)
kernelWriterAssembly = KernelWriterAssembly(asmToolchain.assembler, DebugConfig())
copyStaticFiles(outputPath)
start_wsk = timer()
numKernels, uniqueKernels, kernelInfo = writeSolutionsAndKernelsTCL(
outputPath,
asmToolchain,
srcToolchain,
solutions,
kernels,
kernelHelperObjs,
kernelWriterAssembly,
archs,
arguments["DisableAsmComments"],
compress=arguments["UseCompression"],
removeTemporaries=not arguments["KeepBuildTmp"],
)
stop_wsk = timer()
print(f"Time to generate kernels (s): {(stop_wsk-start_wsk):3.2f}")
archs = [ # is this really different than the other archs above?
isaToGfx(arch)
for arch in targetIsas
if isaInfoMap[arch].asmCaps["SupportedISA"]
]
# Per-base subdirs are created here (idempotent if writeSolutionsAndKernels*
# already created them above). Each per-arch write below routes to its own
# libraryDir(outputPath, archName).
for base in _baseArchs(archs):
ensurePath(libraryDir(outputPath, base))
splitGSU = False
start_pki = timer()
passPostKernelInfoToLibrary(kernelInfo, uniqueKernels, masterLibraries, splitGSU)
stop_pki = timer()
print(f"Time to pass kernel info to library (s): {(stop_pki-start_pki):3.2f}")
solDict = {}
for solution in solutions:
solutionKernels = solution.getKernels()
for kernel in solutionKernels:
kName = getKeyNoInternalArgs(kernel, False)
if kName not in solDict:
solDict["%s"%kName] = kernel
# Split libraryMapping per arch and write one mapping file per arch into
# that arch's per-base subdirectory. Every value ends in "_<arch>" because
# tuned entries carry the arch natively and renameFallbacksPerArch
# arch-suffixed every fallback entry before this point. Filtering on that
# suffix keeps each arch's Mapping complete while letting builds produce
# non-colliding mapping artifacts that survive overlay-style installs.
for archName in archs:
archMapping = {
idx: name
for idx, name in libraryMapping.items()
if name.endswith("_" + archName)
}
if archMapping:
archDir = libraryDir(outputPath, archName)
archMappingFile = os.path.join(
archDir, "TensileLiteLibrary_lazy_" + archName + "_Mapping"
)
LibraryIO.write(archMappingFile, archMapping, "msgpack")
start_msl = timer()
for archName, newMasterLibrary in masterLibraries.items():
if archName in archs:
archDir = libraryDir(outputPath, archName)
def writeMsl(name, lib, archDir=archDir):
filename = os.path.join(archDir, name)
lib.applyNaming(splitGSU)
LibraryIO.write(filename, state(lib), arguments["LibraryFormat"])
if arguments["LazyLibraryLoading"]:
masterFile = os.path.join(archDir, "TensileLibrary_lazy_" + archName)
else:
masterFile = os.path.join(archDir, "TensileLibrary_" + archName)
newMasterLibrary.applyNaming(splitGSU)
LibraryIO.write(masterFile, state(newMasterLibrary), arguments["LibraryFormat"])
ParallelMap2(writeMsl,
newMasterLibrary.lazyLibraries.items(),
"Writing master solution libraries",
return_as="list")
stop_msl = timer()
print(f"Time to write master solution libraries (s): {(stop_msl-start_msl):3.2f}")
if not arguments["KeepBuildTmp"]:
buildTmp = Path(arguments["OutputPath"]).parent / "library" / "build_tmp"
if buildTmp.exists() and buildTmp.is_dir():
shutil.rmtree(buildTmp)
buildTmp = Path(arguments["OutputPath"]) / "build_tmp"
if buildTmp.exists() and buildTmp.is_dir():
shutil.rmtree(buildTmp)
else:
printWarning(f"Cannot remove build_tmp")
print("# Tensile Library Writer DONE")
print(HR)
print("")
stop = timer()
print(f"Total time (s): {(stop-start):3.2f}")
print(f"Total kernels processed: {numKernels}")
print(f"Kernels processed per second: {(numKernels/(stop-start)):3.2f}")
print(f"KernelHelperObjs: {len(kernelHelperObjs)}")