Skip to content

Commit 25abc74

Browse files
authored
Merge pull request #336 from stephenswat/ci/cuda_fp64_check
Add CI functionality to detect FP64 instructions in PTX
2 parents 156353b + 7428e0a commit 25abc74

File tree

3 files changed

+173
-1
lines changed

3 files changed

+173
-1
lines changed

.github/find_f64_ptx.py

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/bin/python3
2+
3+
4+
# TRACCC library, part of the ACTS project (R&D line)
5+
#
6+
# (c) 2023 CERN for the benefit of the ACTS project
7+
#
8+
# Mozilla Public License Version 2.0
9+
10+
11+
import argparse
12+
import re
13+
import collections
14+
import pathlib
15+
16+
17+
class InstructionCounter:
18+
"""
19+
Class for counting the use of certain instructions in translation units.
20+
21+
The instructions and translation units are counted, not linked. So this
22+
class cannot reconstruct that a particular instruction was emitted in a
23+
particular translation unit. But I don't think that's necessary at this
24+
time."""
25+
26+
def __init__(self):
27+
"""
28+
Initialize the counter.
29+
30+
This creates some empty integer dicts with default value zero."""
31+
self.instructions = collections.defaultdict(int)
32+
self.translations = collections.defaultdict(int)
33+
34+
def add(self, instr, trans):
35+
"""Register the occurance of an instruction in a translation unit."""
36+
self.instructions[instr] += 1
37+
self.translations[trans] += 1
38+
39+
40+
def oxford_join(lst):
41+
"""
42+
Format a list of strings in a human-readable way using an Oxford comma.
43+
44+
This function takes ["a", "b", "c"] to the string "a, b, and c"."""
45+
if not lst:
46+
return ""
47+
elif len(lst) == 1:
48+
return str(lst[0])
49+
elif len(lst) == 2:
50+
return f"{str(lst[0])} and {str(lst[1])}"
51+
return f"{', '.join(lst[:-1])}, and {lst[-1]}"
52+
53+
54+
def run(files, source, build):
55+
"""
56+
Perform a search for FP64 instructions in a list of files.
57+
58+
This function takes a list of file paths as well as the root path of the
59+
source code and the build path. These are necessary because the paths
60+
reported in the PTX emitted by NVCC are relative to the build directory. In
61+
order to get the paths relative to the GitHub root directory (which GitHub
62+
Action Commands require), we need to do some path magic."""
63+
# Create a dictionary of counters. The keys in this dictionary are line
64+
# information tuples (source file name and line) and the values are counter
65+
# objects which count how many times that line generates each instruction,
66+
# and how many times it generates instructions in a given translation unit.
67+
counter = collections.defaultdict(InstructionCounter)
68+
69+
# Resolve the source and build paths if they are relevant. Since these are
70+
# constant, we can move this operation out of the loop.
71+
source_path = source.resolve()
72+
build_path = build.resolve()
73+
74+
# Iterate over the list of files that we are given by the user. We do this
75+
# multi-file analysis so we can analyse the mapping of shared source code
76+
# to multiple translation units.
77+
for n in files:
78+
# Read the PTX file and split it into multiple lines. This is NOT a
79+
# proper parsing of PTX and could break, but works for now.
80+
with open(n, "r") as f:
81+
lines = f.read().split("\n")
82+
83+
# At the beginning of the file, the line data is unknown.
84+
linedata = None
85+
86+
# Iterate over the source lines in the PTX.
87+
for l in lines:
88+
if m := re.match(r"^//(?P<file>[/\w\-. ]*):(?P<line>\d+)", l):
89+
# If the line of the form "//[filename]:[line] [code]", we
90+
# parse the file name and line number, then update the line
91+
# data. Any subsequent instructions will be mapped onto this
92+
# source line.
93+
linedata = (m.group("file"), int(m.group("line")))
94+
elif m := re.match(
95+
r"^\s*(?P<instruction>(?:[a-z][a-z0-9]*)(?:\.[a-z][a-z0-9]+)*)", l
96+
):
97+
# If the line is of the form " [instruction] [operands]", we
98+
# parse the instruction. The operands are irrelevant.
99+
if "f64" in m.group("instruction"):
100+
# PTX has the pleasant property that all instructions
101+
# explicitly specify their operand types (Intel x86 syntax
102+
# could learn from this), so if "f64" is contained in the
103+
# instruction it will be a double-precision operator. We
104+
# now proceed to compute the real path of the line that
105+
# produced this instruction.
106+
real_path = (build_path / linedata[0]).resolve()
107+
if linedata is not None:
108+
# If the line data is not none, we have a line to link
109+
# this instruction to. We compute the relative path of
110+
# the source file to the root of the source directory,
111+
# and add the result to the counting dictionary.
112+
try:
113+
counter[
114+
(real_path.relative_to(source_path), linedata[1])
115+
].add(m.group("instruction"), n)
116+
except ValueError:
117+
pass
118+
else:
119+
# If we do not have line data, we register an FP64
120+
# instruction of unknown origin.
121+
counter[None].add(m.group("instruction"), n)
122+
123+
# After we complete our analysis, we print some output to stdout which will
124+
# be parsed by GitHub Actions. For the syntax, please refer to
125+
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
126+
for dt in counter:
127+
instrs = oxford_join(
128+
[f"{counter[dt].instructions[i]} × `{i}`" for i in counter[dt].instructions]
129+
)
130+
units = oxford_join(
131+
[f"`{pathlib.Path(f).name}`" for f in counter[dt].translations]
132+
)
133+
details = (
134+
f"Instruction(s) generated are {instrs} in translation unit(s) {units}."
135+
)
136+
137+
# Handle the cases where the source line information is unknown and
138+
# known, respectively.
139+
if dt is None:
140+
print(
141+
f"::warning title=FP64 instructions emitted in unknown locations::{details}"
142+
)
143+
else:
144+
print(
145+
f"::warning file={dt[0]},line={dt[1]},title=FP64 instructions emitted::{details}"
146+
)
147+
148+
149+
if __name__ == "__main__":
150+
# Construct an argument parser, asking the user for a set of files, as well
151+
# as their source and build directories, in a fashion similar to what CMake
152+
# does.
153+
parser = argparse.ArgumentParser(
154+
description="Find unwanted 64-bit float operations in annotated PTX."
155+
)
156+
157+
parser.add_argument("files", type=str, help="PTX file to use", nargs="+")
158+
parser.add_argument(
159+
"--source", "-S", type=pathlib.Path, help="source directory", required=True
160+
)
161+
parser.add_argument(
162+
"--build", "-B", type=pathlib.Path, help="build directory", required=True
163+
)
164+
165+
args = parser.parse_args()
166+
167+
# Finally, run the analysis!
168+
run(args.files, args.source, args.build)

.github/workflows/builds.yml

+4
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,7 @@ jobs:
5858
cd build
5959
source ${GITHUB_WORKSPACE}/.github/ci_setup.sh ${{ matrix.platform.name }}
6060
ctest --output-on-failure
61+
- name: FP64 Compliance
62+
if: "matrix.platform.name == 'CUDA' && matrix.build == 'Debug'"
63+
continue-on-error: true
64+
run: ${GITHUB_WORKSPACE}/.github/find_f64_ptx.py --source ${GITHUB_WORKSPACE} --build build $(find build -name "*.ptx")

cmake/traccc-compiler-options-cuda.cmake

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ traccc_add_flag( CMAKE_CUDA_FLAGS "--expt-relaxed-constexpr" )
2929

3030
# Make CUDA generate debug symbols for the device code as well in a debug
3131
# build.
32-
traccc_add_flag( CMAKE_CUDA_FLAGS_DEBUG "-G" )
32+
traccc_add_flag( CMAKE_CUDA_FLAGS_DEBUG "-G --keep -src-in-ptx" )
3333

3434
# Ensure that line information is embedded in debugging builds so that
3535
# profilers have access to line data.

0 commit comments

Comments
 (0)