Skip to content

Commit 196aff5

Browse files
committed
fp-stability: per-instance disambiguation of fypp-expanded hotspots (Tier 2)
dd_line attributes to .fpp source lines, but a #:for/#:def expansion collapses many generated computations onto one line, so a macro-ambiguous hotspot cannot be pinned to a single runtime instance. This adds an opt-in precision path that resolves it. Mechanism (validated against gfortran+Verrou): a new build flag --fp-precision-lines strips the fypp line markers from each generated .f90 so the compiler attributes every expanded instance to a distinct physical line, emitting a .linemap.json sidecar mapping each line back to (.fpp file, line, instance). Marker renumbering was tried first but hit gfortran's DWARF line-number ceiling (~300k) and 700-line shadow runs; stripping avoids both and survives the cpp #if layer. fp-stability gains --precision-sim-binary: for the most flagrant macro-ambiguous hotspot, each expanded instance is perturbed alone (Verrou --source) on the precision binary and ranked, naming the responsible instance and showing its concrete generated code. The strip is gated to the simulation target only (pre/post run on CPU). Validated end-to-end: m_weno.fpp:238 (3 #:for instances) resolved to instance #0 = s_cb(i+3)-s_cb(i+1). toolchain/mfc/fp_precision_lines.py is pure + TDD'd (12 tests); normal build path is byte-identical and unaffected.
1 parent ac398ab commit 196aff5

6 files changed

Lines changed: 380 additions & 2 deletions

File tree

CMakeLists.txt

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ option(MFC_DOCUMENTATION "Build documentation" OFF
3131
option(MFC_ALL "Build everything" OFF)
3232
option(MFC_SINGLE_PRECISION "Build single precision" OFF)
3333
option(MFC_MIXED_PRECISION "Build mixed precision" OFF)
34+
option(MFC_FP_PRECISION_LINES "Strip fypp markers for per-instance fp-stability attribution" OFF)
3435

3536
if (MFC_ALL)
3637
set(MFC_PRE_PROCESS ON FORCE)
@@ -433,8 +434,24 @@ macro(HANDLE_SOURCES target useCommon)
433434
cmake_path(GET fpp FILENAME fpp_filename)
434435
set(f90 "${CMAKE_BINARY_DIR}/fypp/${target}/${fpp_filename}.f90")
435436

437+
# In a precision-lines build, Fypp writes a marked intermediate that is
438+
# then stripped of its line markers (so each expanded instance compiles
439+
# to a distinct physical line) before compilation; the strip step emits a
440+
# .linemap.json sidecar. Otherwise Fypp writes ${f90} directly. Only the
441+
# simulation target is analyzed by fp-stability, so pre/post_process are
442+
# always built normally.
443+
set(_precision_lines OFF)
444+
if (MFC_FP_PRECISION_LINES AND "${target}" STREQUAL "simulation")
445+
set(_precision_lines ON)
446+
endif()
447+
if (_precision_lines)
448+
set(f90_out "${CMAKE_BINARY_DIR}/fypp/${target}/${fpp_filename}.marked.f90")
449+
else()
450+
set(f90_out "${f90}")
451+
endif()
452+
436453
add_custom_command(
437-
OUTPUT ${f90}
454+
OUTPUT ${f90_out}
438455
COMMAND ${FYPP_EXE} -m re
439456
-I "${CMAKE_BINARY_DIR}/include/${target}"
440457
-I "${${target}_DIR}/include"
@@ -450,12 +467,25 @@ macro(HANDLE_SOURCES target useCommon)
450467
--line-length=999
451468
--line-numbering-mode=nocontlines
452469
${FYPP_GCOV_OPTS}
453-
"${fpp}" "${f90}"
470+
"${fpp}" "${f90_out}"
454471
DEPENDS "${fpp};${${target}_incs}"
455472
COMMENT "Preprocessing (Fypp) ${fpp_filename}"
456473
VERBATIM
457474
)
458475

476+
if (_precision_lines)
477+
add_custom_command(
478+
OUTPUT ${f90}
479+
COMMAND ${Python3_EXECUTABLE}
480+
"${CMAKE_SOURCE_DIR}/toolchain/mfc/fp_precision_lines.py"
481+
"${f90_out}" "${f90}"
482+
"${CMAKE_BINARY_DIR}/fypp/${target}/${fpp_filename}.linemap.json"
483+
DEPENDS "${f90_out};${CMAKE_SOURCE_DIR}/toolchain/mfc/fp_precision_lines.py"
484+
COMMENT "Stripping markers (fp-precision-lines) ${fpp_filename}"
485+
VERBATIM
486+
)
487+
endif()
488+
459489
list(APPEND ${target}_SRCs ${f90})
460490
endforeach()
461491
endmacro()

toolchain/mfc/build.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ def configure(self, case: Case):
421421
flags.append(f"-DMFC_GCov={'ON' if ARG('gcov') else 'OFF'}")
422422
flags.append(f"-DMFC_Unified={'ON' if ARG('unified') else 'OFF'}")
423423
flags.append(f"-DMFC_Fastmath={'ON' if ARG('fastmath') else 'OFF'}")
424+
flags.append(f"-DMFC_FP_PRECISION_LINES={'ON' if ARG('fp_precision_lines') else 'OFF'}")
424425

425426
command = ["cmake"] + flags + ["-S", cmake_dirpath, "-B", build_dirpath]
426427

toolchain/mfc/cli/commands.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@
141141
default=False,
142142
dest="deps_only",
143143
),
144+
Argument(
145+
name="fp-precision-lines",
146+
help="(fp-stability) Strip fypp line markers so each expanded instance gets a distinct line; emits sidecars for per-instance attribution.",
147+
action=ArgAction.STORE_TRUE,
148+
default=False,
149+
dest="fp_precision_lines",
150+
),
144151
],
145152
examples=[
146153
Example("./mfc.sh build", "Build all default targets (CPU)"),
@@ -938,6 +945,13 @@
938945
default=None,
939946
metavar="PATH",
940947
),
948+
Argument(
949+
name="precision-sim-binary",
950+
help="Path to a simulation binary built with --fp-precision-lines. When given, macro-ambiguous hotspots are disambiguated to the individual fypp-expanded instance.",
951+
default=None,
952+
dest="precision_sim_binary",
953+
metavar="PATH",
954+
),
941955
Argument(
942956
name="samples",
943957
short="N",
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""FP-stability precision-lines transform (Tier 2).
2+
3+
A fypp #:for/#:def expansion emits many generated computations that all carry
4+
the same cpp line marker (`# N "file.fpp"`), so DWARF — and therefore Verrou —
5+
collapse every expanded instance onto one .fpp line. This transform removes the
6+
fypp line markers from a generated .f90 so the compiler attributes each statement
7+
to the generated file's own physical line (which *is* distinct per expanded
8+
instance), and records a sidecar mapping each surviving physical line back to
9+
(file, original .fpp line, instance index). Genuine cpp directives
10+
(#if/#define/#endif/...) are preserved so conditional compilation is unchanged.
11+
12+
When the stripped .f90 is compiled, Verrou attributes — and fp-stability ranks
13+
and isolates via --source — per expanded instance rather than per source line.
14+
Used only by a dedicated precision build (MFC_FP_PRECISION_LINES); the normal
15+
build is unaffected. The mechanism (stripped markers -> instance-distinct
16+
physical-line attribution -> per-instance Verrou --source isolation, surviving
17+
the cpp #if layer) is validated against gfortran + Verrou.
18+
"""
19+
20+
import json
21+
import os
22+
import re
23+
24+
# A fypp line marker: "# <number> "<file>"" possibly with trailing flags. A cpp
25+
# conditional/define directive (#if, #define, #endif, ...) has a word, not a
26+
# number, after the '#', so the two are unambiguous.
27+
_FYPP_MARKER = re.compile(r'^#\s+(\d+)\s+"([^"]+)"')
28+
# Any other preprocessor directive line (kept, but it is not a .fpp source line,
29+
# so it neither consumes a source-line increment nor gets a sidecar entry).
30+
_CPP_DIRECTIVE = re.compile(r"^\s*#")
31+
32+
33+
def strip_markers(lines: list) -> tuple:
34+
"""Strip fypp line markers; return (output_lines, sidecar).
35+
36+
sidecar maps each 1-based physical output line number to
37+
{"file", "line", "instance"}: the .fpp file, the .fpp line that physical
38+
line came from (auto-incremented within a marker region), and how many times
39+
that marker's (file, line) had been seen before (0 = first/real occurrence,
40+
>=1 = an expanded instance).
41+
"""
42+
seen = {}
43+
out = []
44+
sidecar = {}
45+
cur_file = None
46+
cur_line = None
47+
cur_instance = None
48+
for raw in lines:
49+
m = _FYPP_MARKER.match(raw)
50+
if m:
51+
cur_file = m.group(2)
52+
cur_line = int(m.group(1))
53+
cur_instance = seen.get((cur_file, cur_line), 0)
54+
seen[(cur_file, cur_line)] = cur_instance + 1
55+
continue # drop the marker line
56+
out.append(raw)
57+
if cur_file is None or _CPP_DIRECTIVE.match(raw):
58+
# cpp directives are kept verbatim but are not .fpp source lines
59+
continue
60+
sidecar[len(out)] = {"file": cur_file, "line": cur_line, "instance": cur_instance}
61+
cur_line += 1 # subsequent physical source lines map to the next .fpp line
62+
return out, sidecar
63+
64+
65+
def transform_file(in_path: str, out_path: str, sidecar_path: str) -> int:
66+
"""Strip a generated .f90 to its precision-lines variant.
67+
68+
Reads in_path, writes the marker-stripped source to out_path and the sidecar
69+
JSON to sidecar_path. Returns the number of mapped physical lines.
70+
"""
71+
with open(in_path) as fh:
72+
lines = fh.readlines()
73+
out, sidecar = strip_markers(lines)
74+
with open(out_path, "w") as fh:
75+
fh.writelines(out)
76+
with open(sidecar_path, "w") as fh:
77+
json.dump({str(k): v for k, v in sidecar.items()}, fh)
78+
return len(sidecar)
79+
80+
81+
# --- consumption side (Tier 2): locating and querying the sidecars ---
82+
83+
84+
def sidecar_dir_for_binary(sim_bin: str) -> str:
85+
"""Map a precision simulation binary path to its sidecar directory.
86+
87+
.../build/install/<hash>/bin/simulation -> .../build/staging/<hash>/fypp/simulation
88+
"""
89+
bin_dir = os.path.dirname(os.path.abspath(sim_bin)) # .../install/<hash>/bin
90+
hash_dir = os.path.dirname(bin_dir) # .../install/<hash>
91+
cfg_hash = os.path.basename(hash_dir)
92+
build_root = os.path.dirname(os.path.dirname(hash_dir)) # .../build
93+
return os.path.join(build_root, "staging", cfg_hash, "fypp", "simulation")
94+
95+
96+
def sidecar_path(sidecar_dir: str, fpp_file: str) -> str:
97+
"""Sidecar JSON path for a .fpp file: <dir>/<basename>.linemap.json."""
98+
return os.path.join(sidecar_dir, os.path.basename(fpp_file) + ".linemap.json")
99+
100+
101+
def load_sidecar(path: str) -> dict:
102+
"""Load a sidecar JSON into {physical_line:int -> {file, line, instance}}."""
103+
if not os.path.isfile(path):
104+
return {}
105+
with open(path) as fh:
106+
raw = json.load(fh)
107+
return {int(k): v for k, v in raw.items()}
108+
109+
110+
def instances_of(sidecar: dict, fpp_file: str, fpp_line: int) -> list:
111+
"""Return [(physical_line, instance), ...] (sorted by physical line) for every
112+
expanded instance of fpp_file:fpp_line, matched by basename."""
113+
base = os.path.basename(fpp_file)
114+
hits = [(physline, entry["instance"]) for physline, entry in sidecar.items() if os.path.basename(entry["file"]) == base and entry["line"] == fpp_line]
115+
return sorted(hits)
116+
117+
118+
if __name__ == "__main__":
119+
import sys
120+
121+
if len(sys.argv) != 4:
122+
sys.exit("usage: fp_precision_lines.py <in.f90> <out.f90> <sidecar.json>")
123+
transform_file(sys.argv[1], sys.argv[2], sys.argv[3])

toolchain/mfc/fp_stability.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@
4444
One run with --check-max-float=yes; reports locations where a
4545
double→float conversion would overflow to ±Inf.
4646
47+
I. Per-instance disambiguation (--precision-sim-binary PATH; opt-in)
48+
A fypp #:for/#:def expansion collapses many generated computations onto one
49+
.fpp line, so a macro-ambiguous hotspot cannot be pinned to a single runtime
50+
instance. Given a simulation binary built with `--fp-precision-lines` (markers
51+
stripped so each instance is a distinct line, plus .linemap.json sidecars), the
52+
most flagrant macro-ambiguous hotspot is disambiguated: each expanded instance
53+
is perturbed alone on the precision binary, ranking them to the responsible
54+
instance and showing its concrete generated code.
55+
4756
Logs are saved to fp-stability-logs/ and uploaded as CI artifacts.
4857
On GitHub Actions: a step summary table and ::warning:: file annotations
4958
are emitted automatically so failing source lines appear in the PR diff.
@@ -1149,6 +1158,67 @@ def _run_confirmation(case, verrou_bin, sim_bin, work_dir, ref_dir, dd_line_locs
11491158
return confirmed, set_dev, ranked
11501159

11511160

1161+
def _disambiguate_instances(case, prec_sim_bin, verrou_bin, work_dir, hotspot_file, hotspot_line):
1162+
"""Rank the individual fypp-expanded instances of a macro-ambiguous hotspot.
1163+
1164+
Uses a precision binary (built with --fp-precision-lines) in which each
1165+
expanded instance of hotspot_file:hotspot_line compiles to a distinct
1166+
physical .f90 line. The sidecar enumerates those physical lines; each is
1167+
perturbed alone (float mode, vs the precision binary's own nearest-rounding
1168+
reference) so the dominant instance is identified.
1169+
1170+
Returns a list of {instance, physline, dev, snippet} sorted most-flagrant
1171+
first (empty if no sidecar / no instrumented instances).
1172+
"""
1173+
from . import fp_precision_lines as fpl
1174+
1175+
sidecar_dir = fpl.sidecar_dir_for_binary(prec_sim_bin)
1176+
sidecar = fpl.load_sidecar(fpl.sidecar_path(sidecar_dir, hotspot_file))
1177+
instances = fpl.instances_of(sidecar, hotspot_file, hotspot_line)
1178+
if not instances:
1179+
return []
1180+
1181+
prec_dir = os.path.join(work_dir, "precision")
1182+
ref_dir = os.path.join(prec_dir, "ref")
1183+
os.makedirs(ref_dir, exist_ok=True)
1184+
gen_path = os.path.join(prec_dir, "gen_source.txt")
1185+
try:
1186+
_run_simulation_verrou(verrou_bin, prec_sim_bin, work_dir, ref_dir, rounding_mode="nearest")
1187+
_run_simulation_verrou(
1188+
verrou_bin,
1189+
prec_sim_bin,
1190+
work_dir,
1191+
prec_dir,
1192+
rounding_mode="nearest",
1193+
extra_flags=[f"--gen-source={gen_path}"],
1194+
)
1195+
except MFCException:
1196+
return []
1197+
if not os.path.isfile(gen_path):
1198+
return []
1199+
with open(gen_path) as fh:
1200+
gen_lines = fh.readlines()
1201+
1202+
f90_file = os.path.join(sidecar_dir, os.path.basename(hotspot_file) + ".f90")
1203+
compare = case["compare"]
1204+
results = []
1205+
for physline, instance in instances:
1206+
src = _build_source_filter(gen_lines, [(f90_file, physline, physline)])
1207+
if not src:
1208+
continue # this instance performs no instrumented FP op
1209+
dev = _source_perturb_dev(verrou_bin, prec_sim_bin, work_dir, ref_dir, prec_dir, src, compare, f"inst{instance:02d}")
1210+
results.append(
1211+
{
1212+
"instance": instance,
1213+
"physline": physline,
1214+
"dev": dev or 0.0,
1215+
"snippet": _read_source_line(f90_file, physline).strip(),
1216+
}
1217+
)
1218+
results.sort(key=lambda r: r["dev"], reverse=True)
1219+
return results
1220+
1221+
11521222
def _run_case(
11531223
case: dict,
11541224
verrou_bin: str,
@@ -1163,6 +1233,7 @@ def _run_case(
11631233
run_cancellation: bool,
11641234
run_mca: bool,
11651235
run_float_max: bool,
1236+
prec_sim_bin: str = None,
11661237
) -> dict:
11671238
name = case["name"]
11681239
threshold = case["threshold"]
@@ -1294,6 +1365,24 @@ def _run_case(
12941365
except Exception as exc:
12951366
cons.print(f" [bold yellow]dd_line confirmation error[/bold yellow]: {exc}")
12961367

1368+
# --- E3: per-instance disambiguation of the most flagrant macro-ambiguous hotspot ---
1369+
if prec_sim_bin and result["dd_line_locs"]:
1370+
macro_loc = next((loc for loc in result["dd_line_locs"] if loc.get("macro")), None)
1371+
if macro_loc:
1372+
cons.print(f" [dim]disambiguating fypp instances of {macro_loc['path']}:{macro_loc['start']} (precision binary)...[/dim]")
1373+
try:
1374+
insts = _disambiguate_instances(case, prec_sim_bin, verrou_bin, work_dir, macro_loc["path"], macro_loc["start"])
1375+
macro_loc["instances"] = insts
1376+
if insts and insts[0]["dev"] > 0:
1377+
win = insts[0]
1378+
cons.print(f" flagrant instance: #{win['instance']} (.f90:{win['physline']}, dev={win['dev']:.3e}) {win['snippet']}")
1379+
elif insts:
1380+
cons.print(f" [dim]{len(insts)} instance(s) enumerated; none perturbed measurably (hotspot inert)[/dim]")
1381+
else:
1382+
cons.print(" [dim]no sidecar instances found for this hotspot[/dim]")
1383+
except Exception as exc:
1384+
cons.print(f" [bold yellow]instance disambiguation error[/bold yellow]: {exc}")
1385+
12971386
# --- F: cancellation detection ---
12981387
if run_cancellation:
12991388
cons.print(" [dim]cancellation detection...[/dim]")
@@ -1460,6 +1549,9 @@ def _emit_github_summary(results: list, n_samples: int):
14601549
tags.append(f"_{loc['macro']}-expanded, may represent multiple instances_")
14611550
suffix = f" — {', '.join(tags)}" if tags else ""
14621551
md.append(f"- `{where}`{suffix}")
1552+
for inst in loc.get("instances", [])[:8]:
1553+
flag = " ⟵ flagrant" if inst is loc["instances"][0] and inst["dev"] > 0 else ""
1554+
md.append(f" - instance #{inst['instance']} (`.f90:{inst['physline']}`, dev={inst['dev']:.2e}){flag}: `{inst['snippet']}`")
14631555
snippet = _get_source_context(rel_path, start)
14641556
if snippet:
14651557
md.append(" ```fortran")
@@ -1531,6 +1623,9 @@ def fp_stability():
15311623
run_cancellation = not ARG("no_cancellation")
15321624
run_mca = not ARG("no_mca")
15331625
run_float_max = not ARG("no_float_max")
1626+
prec_sim_bin = ARG("precision_sim_binary")
1627+
if prec_sim_bin and not os.path.isfile(prec_sim_bin):
1628+
raise MFCException(f"precision simulation binary not found: {prec_sim_bin}")
15341629

15351630
log_dir = os.path.join(MFC_ROOT_DIR, "fp-stability-logs")
15361631
os.makedirs(log_dir, exist_ok=True)
@@ -1540,6 +1635,8 @@ def fp_stability():
15401635
cons.print(f" verrou: {verrou_bin}")
15411636
cons.print(f" simulation: {sim_bin}")
15421637
cons.print(f" pre_process: {pp_bin}")
1638+
if prec_sim_bin:
1639+
cons.print(f" precision: {prec_sim_bin} (per-instance disambiguation)")
15431640
cons.print(f" samples: {n_samples}")
15441641
features = []
15451642
if run_float:
@@ -1578,6 +1675,7 @@ def fp_stability():
15781675
run_cancellation,
15791676
run_mca,
15801677
run_float_max,
1678+
prec_sim_bin,
15811679
)
15821680
except MFCException as exc:
15831681
cons.print(f" [bold red]ERROR[/bold red]: {exc}")

0 commit comments

Comments
 (0)