diff --git a/.github/scripts/check-kernel-isolation.py b/.github/scripts/check-kernel-isolation.py index a18f552..a8f61f9 100644 --- a/.github/scripts/check-kernel-isolation.py +++ b/.github/scripts/check-kernel-isolation.py @@ -2,6 +2,9 @@ GCC and Clang objects must define no shared functions. MSVC objects may define them with baseline instructions only. + +The LLVM tools read the ELF, Mach-O and COFF objects alike, the CI +environment installs them with llvm-tools. """ import re @@ -10,41 +13,43 @@ from pathlib import Path OBJECT_NAME = re.compile( - r"CalculationKernel(SSE2|AVX2|AVX512|NEON)(?:\.cpp)?\.(?:o|obj)$" -) -COFF_FUNCTION = re.compile( - r"SECT[0-9A-F]+\s+notype \(\)\s+External\s+\| (\S+)" -) -INSTRUCTION = re.compile( - r"^\s+[0-9A-F]+:\s+((?:[0-9A-F]{2}\s+)+)([a-z?][a-z0-9?]*)(?:\s|$)" + r"CalculationKernel(SSE2|AVX2|AVX512|NEON)(?:\.cpp)?\.(o|obj)$" ) +NM_LINE = re.compile(r"(.+) ([A-Za-z?]) [0-9a-fA-F]*(?: .*)?") +FUNCTION_TYPES = {"T", "W", "i", "I"} +LABEL = re.compile(r"^[0-9a-f]+ <(.+)>:$") +INSTRUCTION = re.compile(r"^\s+[0-9a-f]+:\s+((?:[0-9a-f]{2} )+)\s*([a-z]\S*)") # Instructions above the baseline that have no VEX prefix. EXTRA_ISA = {"tzcnt", "lzcnt", "popcnt", "crc32"} def run(*command): - return subprocess.run( - command, check=True, capture_output=True, text=True, errors="replace" - ).stdout + result = subprocess.run( + command, capture_output=True, text=True, errors="replace" + ) + if result.returncode != 0: + raise SystemExit( + f"{' '.join(command)} failed with exit status " + f"{result.returncode}:\n{result.stderr}" + ) + return result.stdout def private_symbol(name, isa): - namespace = f"xsimd_{isa.lower()}" - if f"{namespace}::" in name or f"@{namespace}@@" in name: - return True - return "(anonymous namespace)" in name or "?A0x" in name + return ( + f"xsimd_{isa.lower()}::" in name + or "(anonymous namespace)" in name + or "`anonymous namespace'" in name + ) def kernel_entry(name, isa): - entry = f"{isa.lower()}CalculationKernel" - return name == f"diffpy::srreal::kernels::{entry}()" or name.startswith( - f"?{entry}@kernels@srreal@diffpy@@" - ) + return f"diffpy::srreal::kernels::{isa.lower()}CalculationKernel(" in name -def check_nm(path, isa): +def defined_functions(path): output = run( - "nm", + "llvm-nm", "--defined-only", "--extern-only", "--demangle", @@ -53,57 +58,74 @@ def check_nm(path, isa): ) functions = [] for line in output.splitlines(): - match = re.fullmatch(r"(.+) ([A-Za-z?]) [0-9a-fA-F]+(?: .*)?", line) + match = NM_LINE.fullmatch(line) if not match: - raise ValueError(f"Unrecognized nm output: {line}") + raise ValueError(f"Unrecognized llvm-nm output: {line}") name, kind = match.groups() - if kind in {"T", "W", "i", "I"}: + if kind in FUNCTION_TYPES: functions.append(name) - if not any(kernel_entry(name, isa) for name in functions): - raise ValueError(f"Missing {isa} entry point in {path}") - return [ - f"shared code symbol: {name}" - for name in functions - if not private_symbol(name, isa) and not kernel_entry(name, isa) - ] + return functions -def check_coff(path, isa): - symbols = run("dumpbin", "/nologo", "/symbols", str(path)) - functions = COFF_FUNCTION.findall(symbols) - if not any(kernel_entry(name, isa) for name in functions): - raise ValueError(f"Missing {isa} entry point in {path} (LTO object?)") - shared = { - name - for name in functions - if not private_symbol(name, isa) and not kernel_entry(name, isa) - } - output = run("dumpbin", "/nologo", "/disasm", str(path)) +def disassembly(path): + """Instructions of the functions in path as (bytes, mnemonic) by + name.""" + output = run( + "llvm-objdump", + "--disassemble", + "--demangle", + "--x86-asm-syntax=intel", + str(path), + ) bodies = {} current = None for line in output.splitlines(): - # A function starts with its decorated name. - if line and not line[0].isspace() and line.endswith(":"): - current = line.split(" ", 1)[0].rstrip(":") - bodies[current] = [] - elif current is not None: - match = INSTRUCTION.match(line) - if match: - bodies[current].append(match.groups()) + label = LABEL.match(line) + if label: + current = bodies.setdefault(label[1], []) + continue + match = INSTRUCTION.match(line) + if match and current is not None: + current.append(match.groups()) + return bodies + + +def non_baseline(body): + for raw, mnemonic in body: + # c4 and c5 start VEX instructions, 62 starts EVEX ones. + if ( + raw.split()[0] in {"c4", "c5", "62"} + or mnemonic.startswith("v") + or mnemonic in EXTRA_ISA + ): + return mnemonic + return None + + +def check(path, isa, msvc): + functions = defined_functions(path) + if not any(kernel_entry(name, isa) for name in functions): + raise ValueError(f"Missing {isa} entry point in {path} (LTO object?)") + shared = sorted( + { + name + for name in functions + if not private_symbol(name, isa) and not kernel_entry(name, isa) + } + ) + if not msvc: + return [f"shared code symbol: {name}" for name in shared] + bodies = disassembly(path) errors = [] - for name in sorted(shared): + for name in shared: if not bodies.get(name): - raise ValueError(f"No disassembly for shared function {name}") - for raw, mnemonic in bodies[name]: - # C4 and C5 start VEX instructions, 62 starts EVEX ones. - if ( - raw.split()[0] in {"C4", "C5", "62"} - or mnemonic.startswith("v") - or mnemonic in EXTRA_ISA - or "?" in mnemonic - ): - errors.append(f"non-baseline instruction {mnemonic}: {name}") - break + raise ValueError( + f"No disassembly for shared function {name} in {path}, " + f"{len(bodies)} functions disassembled" + ) + mnemonic = non_baseline(bodies[name]) + if mnemonic: + errors.append(f"non-baseline instruction {mnemonic}: {name}") return errors @@ -116,8 +138,7 @@ def main(): if not match: continue found = True - check = check_coff if path.suffix == ".obj" else check_nm - errors = check(path, match[1]) + errors = check(path, match[1], msvc=match[2] == "obj") if errors: failures.extend(f"{path}: {error}" for error in errors) else: diff --git a/.github/scripts/configure-tests.py b/.github/scripts/configure-tests.py index aac64f6..9f07506 100644 --- a/.github/scripts/configure-tests.py +++ b/.github/scripts/configure-tests.py @@ -3,7 +3,8 @@ import json from pathlib import Path -requirements = ["python"] +# llvm-tools for check-kernel-isolation.py +requirements = ["python", "llvm-tools"] for filename in ("build.txt", "conda.txt", "tests.txt"): for line in (Path("requirements") / filename).read_text().splitlines(): spec = line.split("#", 1)[0].strip() diff --git a/.github/workflows/matrix-and-codecov.yml b/.github/workflows/matrix-and-codecov.yml index 88701b4..bbf24e8 100644 --- a/.github/workflows/matrix-and-codecov.yml +++ b/.github/workflows/matrix-and-codecov.yml @@ -78,6 +78,16 @@ jobs: - name: Check SIMD kernel isolation run: python .github/scripts/check-kernel-isolation.py build + - name: Save SIMD kernel objects + if: failure() + uses: actions/upload-artifact@v4 + with: + name: kernel-objects-${{ matrix.os }} + path: | + build/**/CalculationKernel*.o + build/**/CalculationKernel*.obj + if-no-files-found: ignore + - name: Generate coverage report if: matrix.coverage == 'ON' run: >- diff --git a/news/kernel.rst b/news/kernel.rst new file mode 100644 index 0000000..a32eb18 --- /dev/null +++ b/news/kernel.rst @@ -0,0 +1,23 @@ +**Added:** + +* No news needed: compare the batched bond vectors within 1e-12 and check the SIMD kernel isolation with the LLVM tools on all CI platforms. Not user facing. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/tests/TestBondGeneratorBatching.cpp b/src/tests/TestBondGeneratorBatching.cpp index 33bb24f..f422e24 100644 --- a/src/tests/TestBondGeneratorBatching.cpp +++ b/src/tests/TestBondGeneratorBatching.cpp @@ -142,11 +142,14 @@ std::vector generated(BaseBondGenerator& bnds) { return rv; } +/// Equality of the bond vectors. +const diffpy::mathutils::EpsilonEqual allclose(1e-12); + bool sameBond(const Bond& a, const Bond& e) { bool rv = (a.site0 == e.site0) && (a.site1 == e.site1) && - (std::fabs(a.distance - e.distance) <= 1e-14 * e.distance); + (std::fabs(a.distance - e.distance) <= 1e-14 * e.distance) && + allclose(a.r1, e.r1) && allclose(a.r01, e.r01); for (int k = 0; k < 3; ++k) { - rv = rv && (a.r1[k] == e.r1[k]) && (a.r01[k] == e.r01[k]); for (int l = 0; l < 3; ++l) rv = rv && (a.u1(k, l) == e.u1(k, l)); } return rv; @@ -163,10 +166,8 @@ void checkSameBonds(const std::vector& actual, const Bond& e = expected[i]; CHECK(a.site0 == e.site0); CHECK(a.site1 == e.site1); - for (int k = 0; k < 3; ++k) { - CHECK(a.r1[k] == e.r1[k]); - CHECK(a.r01[k] == e.r01[k]); - } + CHECK(allclose(a.r1, e.r1)); + CHECK(allclose(a.r01, e.r01)); CHECK(std::fabs(a.distance - e.distance) <= 1e-14 * e.distance); for (int k = 0; k < 3; ++k) { for (int l = 0; l < 3; ++l) CHECK(a.u1(k, l) == e.u1(k, l));