Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 84 additions & 63 deletions .github/scripts/check-kernel-isolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand All @@ -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


Expand All @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion .github/scripts/configure-tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/matrix-and-codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >-
Expand Down
23 changes: 23 additions & 0 deletions news/kernel.rst
Original file line number Diff line number Diff line change
@@ -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:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
13 changes: 7 additions & 6 deletions src/tests/TestBondGeneratorBatching.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,14 @@ std::vector<Bond> 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;
Expand All @@ -163,10 +166,8 @@ void checkSameBonds(const std::vector<Bond>& 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));
Expand Down
Loading