Skip to content

Commit 1d8c052

Browse files
committed
Bugfix for Fortran+C++ code generation
1 parent 98f7711 commit 1d8c052

5 files changed

Lines changed: 259 additions & 11 deletions

File tree

modules/COEN/CppOptimize.m

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,44 @@ These are global memory reads (~400 cycle latency) and must be computed once.
5050
<|"Expr" -> newExpr, "Definitions" -> Table[{names[[i]], uniqueCalls[[i]]}, {i, 1, Length[uniqueCalls]}], "Count" -> Length[uniqueCalls]|>
5151
];
5252

53+
(**********************************************************************************
54+
Pass 1b: Composite-Denominator Hoisting
55+
Extract every distinct negative-integer power of a composite (Plus/Times) base
56+
-- the regulated-propagator denominators 1/(M^2 + q*(...)^2) etc. These are NOT
57+
interpolator leaves, so without global hoisting they fall to the per-sub-kernel
58+
CSE (Pass 3) and are recomputed once per chunk when the term-sum is split.
59+
Hoisting them to shared "_den" defs (like interpolators) computes each once.
60+
61+
Bases are sorted by LeafCount ascending so a denominator nested inside another
62+
gets a lower index; each def body then substitutes the already-assigned inner
63+
"_denK" (rules without its own), making the def list topologically ordered.
64+
Runs AFTER interpolator hoisting, so bases already carry "_interp" placeholders.
65+
**********************************************************************************)
66+
67+
hoistDivisions[expr_] :=
68+
Module[{cands, uniqueDens, names, rules, newExpr, defs},
69+
If[Not @ TrueQ[$codeHoistDivisions],
70+
Return[<|"Expr" -> expr, "Definitions" -> {}, "Count" -> 0|>]
71+
];
72+
(* composite reciprocals: Power[base, n<0] with base a Plus or Times (a shared,
73+
non-trivial denominator), not a bare symbol/number/placeholder *)
74+
cands = Cases[expr,
75+
Power[b_, n_Integer] /; n < 0 && (Head[b] === Plus || Head[b] === Times),
76+
Infinity];
77+
(* inner (smaller) denominators first -> def list is dependency-ordered *)
78+
uniqueDens = SortBy[DeleteDuplicates[cands], LeafCount];
79+
If[Length[uniqueDens] === 0,
80+
Return[<|"Expr" -> expr, "Definitions" -> {}, "Count" -> 0|>]
81+
];
82+
names = Table["_den" <> ToString[i], {i, 1, Length[uniqueDens]}];
83+
rules = Thread[uniqueDens -> names];
84+
newExpr = expr //. rules;
85+
(* def body for _denI: replace NESTED denominators (all rules except its own,
86+
which would otherwise collapse the whole body to its own name) *)
87+
defs = Table[{names[[i]], uniqueDens[[i]] //. Drop[rules, {i}]}, {i, 1, Length[uniqueDens]}];
88+
<|"Expr" -> newExpr, "Definitions" -> defs, "Count" -> Length[uniqueDens]|>
89+
];
90+
5391
(**********************************************************************************
5492
Power Basis Normalization
5593
After CSE, rewrite Power[base, m] in terms of an already-hoisted Power[base, n]
@@ -407,11 +445,35 @@ Partitions interps into shared (referenced by 2+ kernels) and local (1 kernel).
407445
interpNames = #[[1]]& /@ interpDefs;
408446
interpsByName = Association @ Table[interpDefs[[i, 1]] -> interpDefs[[i]], {i, Length[interpDefs]}];
409447
Module[
410-
{chunkRefs, useCounts, sharedNames, sharedDefs}
448+
{chunkRefs, useCounts, sharedNames, sharedDefs, defsByName}
411449
,
412-
(* Find which interp names each chunk references *)
413-
chunkRefs = Map[Intersection[interpNames, DeleteDuplicates @ Cases[#, _String, Infinity]]&, chunks];
414-
(* Count how many chunks reference each interp *)
450+
(* name -> definition, for transitive reference expansion *)
451+
defsByName = Association @ Table[interpDefs[[i, 1]] -> interpDefs[[i]], {i, Length[interpDefs]}];
452+
(* Find which def names each chunk references -- TRANSITIVELY: a chunk that
453+
references _den5 also "uses" every _interp/_den that _den5's body reads,
454+
so a denominator shared across chunks (and its inputs) is correctly counted
455+
as shared rather than dropped or recomputed per chunk. *)
456+
chunkRefs =
457+
Map[
458+
Function[{chunk},
459+
Module[{refs, prevLen = -1},
460+
(* level {0, Infinity}: a chunk (or def body) that IS a bare
461+
placeholder string -- e.g. a term that is a single hoisted
462+
reciprocal _denK with no other factors -- sits at level 0 and
463+
would be missed by the default Infinity (= {1, Infinity}). *)
464+
refs = Intersection[interpNames, DeleteDuplicates @ Cases[chunk, _String, {0, Infinity}]];
465+
While[Length[refs] =!= prevLen,
466+
prevLen = Length[refs];
467+
refs = DeleteDuplicates @ Join[refs,
468+
Intersection[interpNames,
469+
Flatten @ Map[Cases[defsByName[#][[2]], _String, {0, Infinity}]&, refs]]];
470+
];
471+
refs
472+
]
473+
],
474+
chunks
475+
];
476+
(* Count how many chunks reference each interp/den *)
415477
useCounts = Counts[Flatten[chunkRefs]];
416478
sharedNames = Keys @ Select[useCounts, # > 1&];
417479
sharedDefs = Select[interpDefs, MemberQ[sharedNames, #[[1]]]&];
@@ -583,7 +645,7 @@ Partitions interps into shared (referenced by 2+ kernels) and local (1 kernel).
583645
**********************************************************************************)
584646

585647
optimizeExpression[equation_] :=
586-
Module[{expr, interpResult, interpCount, splitResult, sharedDefs, subKernels, result, allDefs},
648+
Module[{expr, interpResult, interpCount, divResult, divCount, globalDefs, splitResult, sharedDefs, subKernels, result, allDefs},
587649
FunKitDebug[1, "Starting optimization pipeline (optimize = ", $codeOptimize, ")"];
588650
(* If optimization is disabled, return raw expression with no passes *)
589651
If[!TrueQ[$codeOptimize],
@@ -596,8 +658,15 @@ Partitions interps into shared (referenced by 2+ kernels) and local (1 kernel).
596658
expr = interpResult["Expr"];
597659
interpCount = interpResult["Count"];
598660
FunKitDebug[2, "Hoisted ", interpCount, " interpolator calls"];
661+
(* Pass 1b: Composite-denominator hoisting — share reciprocals across chunks *)
662+
divResult = cgTimed[$ProfileCgHoist, hoistDivisions[expr]];
663+
expr = divResult["Expr"];
664+
divCount = divResult["Count"];
665+
FunKitDebug[2, "Hoisted ", divCount, " composite denominators"];
666+
(* global defs: interps FIRST, then denominators (which reference interps) *)
667+
globalDefs = Join[interpResult["Definitions"], divResult["Definitions"]];
599668
(* Pass 2: Early split decision *)
600-
splitResult = cgTimed[$ProfileCgSplit, earlySplit[interpResult["Definitions"], expr]];
669+
splitResult = cgTimed[$ProfileCgSplit, earlySplit[globalDefs, expr]];
601670
FunKitDebug[2, "Early split: ", splitResult["Split"]];
602671
(* === PER-KERNEL PASSES === *)
603672
If[TrueQ[splitResult["Split"]],
@@ -623,8 +692,8 @@ Partitions interps into shared (referenced by 2+ kernels) and local (1 kernel).
623692
<|"UseSubKernels" -> True, "SharedDefinitions" -> sharedDefs, "SubKernels" -> subKernels|>
624693
,
625694
(* Single-kernel path: optimize the whole expression *)
626-
result = optimizeSubKernel[expr, interpCount];
627-
allDefs = Join[interpResult["Definitions"], result["Definitions"]];
695+
result = optimizeSubKernel[expr, interpCount + divCount];
696+
allDefs = Join[globalDefs, result["Definitions"]];
628697
FunKitDebug[2, "Single-kernel optimization complete: ", Length[allDefs], " total defs"];
629698
(* Try splitting for registers if expression is large *)
630699
splitResult = cgTimed[$ProfileCgSplit, splitIntoSubKernels[allDefs, result["Expr"]]];

modules/COEN/Fortran.m

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,20 @@
3131
];
3232

3333
(* Fortran identifiers must start with a letter. The optimizer produces names
34-
like _interp1, _cse1, _tran1, _result1 which are invalid. The sub-kernel
34+
like _interp1, _cse1, _tran1, _result1, _den1 which are invalid. The sub-kernel
3535
accumulator _acc and the ReturnTransform placeholder postAcc<n> are also
3636
coerced. This function renames them by stripping any leading underscore and
3737
adding an "fk" prefix. *)
3838
fortranFixNames[code_String] :=
3939
StringReplace[code, {
40-
"_" ~~ prefix:("interp"|"cse"|"tran"|"result") ~~ num:DigitCharacter.. :> "fk" <> prefix <> num,
40+
"_" ~~ prefix:("interp"|"cse"|"tran"|"result"|"den") ~~ num:DigitCharacter.. :> "fk" <> prefix <> num,
4141
WordBoundary ~~ "_acc" ~~ WordBoundary :> "fkacc"
4242
}];
4343

4444
(* Generate double precision declarations for all optimizer variables in the code *)
4545
fortranVarDeclarations[code_String] :=
4646
Module[{numbered, accVar, vars},
47-
numbered = Union @ StringCases[code, "fk" ~~ ("interp"|"cse"|"tran"|"result") ~~ DigitCharacter..];
47+
numbered = Union @ StringCases[code, "fk" ~~ ("interp"|"cse"|"tran"|"result"|"den") ~~ DigitCharacter..];
4848
accVar = If[StringContainsQ[code, WordBoundary ~~ "fkacc" ~~ WordBoundary], {"fkacc"}, {}];
4949
vars = Join[numbered, accVar];
5050
If[Length[vars] === 0, "",

modules/COEN/Tools.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@ Codegen profiling instrumentation (zero overhead unless enabled)
188188

189189
$codeOptimizeInterps = {a_Symbol[__] /; Not @ MatchQ[a, Times | Plus | Power | Rational | Complex | Real | Integer]};
190190

191+
(* Composite-denominator (negative integer power of a Plus/Times base) global hoisting.
192+
These reciprocals are NOT interpolator leaves, so without this they fall to the
193+
PER-SUB-KERNEL CSE and get recomputed once per chunk when the term-sum is split.
194+
Hoisting them to shared defs (like interpolators) computes each once across chunks. *)
195+
$codeHoistDivisions = True;
196+
191197
$availableRegisters = 32;
192198

193199
$codeOptimize = True;

tests/COEN/CppTests.m

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,112 @@ int main () {
268268

269269
AppendTo[tests, VerificationTest[StringContainsQ[tranCode, "_tran"], True, TestID -> "Verify transcendental hoisting creates _tran variables"]];
270270

271+
(**********************************************************************************
272+
Composite-denominator hoisting (Pass 1b)
273+
Negative integer powers of a Plus/Times base — the regulated-propagator
274+
denominators 1/(M^2 + q*(...)^2) — are hoisted to shared "_den" defs so a
275+
denominator shared across split sub-kernels is computed once, not recomputed
276+
per chunk by the per-sub-kernel CSE. See CppOptimize.m hoistDivisions and
277+
Tools.m $codeHoistDivisions.
278+
**********************************************************************************)
279+
280+
ClearAll[a, mm, b, c];
281+
282+
(* A composite denominator is hoisted to a _den variable. *)
283+
284+
Block[{FunKit`Private`$codeOptimize = True, FunKit`Private`$codeHoistDivisions = True},
285+
denHoistCode = CppCode[Sin[a] / (mm + a^2) + Cos[a] / (mm + a^2)];
286+
];
287+
288+
AppendTo[tests, VerificationTest[StringContainsQ[denHoistCode, "_den"], True, TestID -> "Composite denominator hoisting creates _den variables"]];
289+
290+
(* With hoisting disabled the same denominator falls to the per-kernel CSE, so no
291+
_den appears — guards the $codeHoistDivisions toggle. *)
292+
293+
Block[{FunKit`Private`$codeOptimize = True, FunKit`Private`$codeHoistDivisions = False},
294+
denNoHoistCode = CppCode[Sin[a] / (mm + a^2) + Cos[a] / (mm + a^2)];
295+
];
296+
297+
AppendTo[tests, VerificationTest[StringFreeQ[denNoHoistCode, "_den"], True, TestID -> "No _den variables when $codeHoistDivisions is False"]];
298+
299+
(* A denominator shared across many terms that split into sub-kernels must be
300+
hoisted to exactly ONE shared def (the efficiency invariant: not recomputed
301+
once per chunk). Exercises the transitive reference tracking in earlySplit. *)
302+
303+
ClearAll[a];
304+
denSharedExpr = Sum[Sin[a + i] Cos[a - i] / (3 + a^2), {i, 1, 600}];
305+
306+
Block[{FunKit`Private`$codeOptimize = True, FunKit`Private`$codeHoistDivisions = True, FunKit`Private`$codeMaxKernelTerms = 200},
307+
denSharedCode = CppCode[denSharedExpr];
308+
];
309+
310+
AppendTo[tests, VerificationTest[StringContainsQ[denSharedCode, "// subkernel 1"], True, TestID -> "Shared denominator: expression does split into sub-kernels"]];
311+
312+
AppendTo[tests, VerificationTest[Length @ StringCases[denSharedCode, "const auto _den" ~~ DigitCharacter.. ~~ " ="], 1, TestID -> "Shared denominator hoisted to a single def across sub-kernels"]];
313+
314+
(* ...and the split kernel still computes the right value. *)
315+
316+
Block[{FunKit`Private`$codeOptimize = True, FunKit`Private`$codeHoistDivisions = True, FunKit`Private`$codeMaxKernelTerms = 200},
317+
funBodyDen = MakeCppFunction[denSharedExpr, "Name" -> "funDen", "Body" -> "using namespace std; const auto a = in;", "Parameters" -> {"in"}];
318+
];
319+
320+
execDen = CreateExecutable["
321+
#include <iostream>
322+
#include <iomanip>
323+
#include <cmath>
324+
using NumberType = double;
325+
326+
" <> fmaCode <> "
327+
" <> powrCode <> "
328+
" <> funBodyDen <> "
329+
330+
int main () {
331+
std::cout << std::setprecision (10) << funDen (1.5) << std::endl;
332+
}
333+
", "FunKitCppTestDen", "CompilerName" -> CppCompiler, "SystemCompileOptions" -> "-std=c++20"];
334+
335+
outputDen = Import["!" <> QuoteFile[execDen], "Text"];
336+
337+
expectedDen = ToString[NumberForm[denSharedExpr /. a -> 1.5, 10]];
338+
339+
AppendTo[tests, VerificationTest[execDen =!= $Failed, True, TestID -> "Shared denominator hoisting compiles with sub-kernel splitting"]];
340+
341+
AppendTo[tests, VerificationTest[outputDen, expectedDen, TestID -> "Shared denominator hoisting preserves numerical value across sub-kernels"]];
342+
343+
(* Nested denominators: inner is hoisted first and the outer def references it
344+
(topological ordering by LeafCount + nested substitution). Compile & run to
345+
confirm the dependency-ordered defs produce the correct value. *)
346+
347+
ClearAll[a, b, c];
348+
denNestedExpr = 1 / (b + 1 / (c + a^2)) + a / (c + a^2);
349+
350+
Block[{FunKit`Private`$codeOptimize = True, FunKit`Private`$codeHoistDivisions = True},
351+
funBodyNested = MakeCppFunction[denNestedExpr /. {b -> 0.7, c -> 1.3}, "Name" -> "funNested", "Body" -> "using namespace std; const auto a = in;", "Parameters" -> {"in"}];
352+
];
353+
354+
execNested = CreateExecutable["
355+
#include <iostream>
356+
#include <iomanip>
357+
#include <cmath>
358+
using NumberType = double;
359+
360+
" <> fmaCode <> "
361+
" <> powrCode <> "
362+
" <> funBodyNested <> "
363+
364+
int main () {
365+
std::cout << std::setprecision (10) << funNested (1.5) << std::endl;
366+
}
367+
", "FunKitCppTestNested", "CompilerName" -> CppCompiler, "SystemCompileOptions" -> "-std=c++20"];
368+
369+
outputNested = Import["!" <> QuoteFile[execNested], "Text"];
370+
371+
expectedNested = ToString[NumberForm[denNestedExpr /. {b -> 0.7, c -> 1.3, a -> 1.5}, 10]];
372+
373+
AppendTo[tests, VerificationTest[execNested =!= $Failed, True, TestID -> "Nested denominator hoisting compiles"]];
374+
375+
AppendTo[tests, VerificationTest[outputNested, expectedNested, TestID -> "Nested denominator hoisting preserves numerical value"]];
376+
271377
(**********************************************************************************
272378
ReturnTransform option (post-processing injection)
273379
**********************************************************************************)

tests/COEN/FortranTests.m

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,73 @@ Basic Fortran Function Test (MakeFortranFunction)
121121

122122
AppendTo[tests, VerificationTest[Abs[fortranOptVal - expectedOptVal] < 1*^-8, True, TestID -> "Optimized Fortran function returns correct value"]];
123123

124+
(**********************************************************************************
125+
Composite-denominator hoisting: _den names must be coerced to valid Fortran
126+
identifiers (fkden), since Fortran identifiers cannot start with "_". See
127+
Fortran.m fortranFixNames / fortranVarDeclarations and CppOptimize.m
128+
hoistDivisions.
129+
**********************************************************************************)
130+
131+
ClearAll[a, mm]
132+
133+
Block[{FunKit`Private`$codeOptimize = True, FunKit`Private`$codeHoistDivisions = True},
134+
fortranDenCode = FortranCode[Sin[a] / (mm + a^2) + Cos[a] / (mm + a^2)];
135+
];
136+
137+
AppendTo[tests, VerificationTest[
138+
StringContainsQ[fortranDenCode, "fkden"],
139+
True,
140+
TestID -> "Composite denominator hoisting produces fkden variables in Fortran"
141+
]];
142+
143+
(* No leading-underscore _den survives the rename (would be invalid Fortran). *)
144+
145+
AppendTo[tests, VerificationTest[
146+
StringFreeQ[fortranDenCode, "_den"],
147+
True,
148+
TestID -> "No invalid _den identifiers remain in Fortran output"
149+
]];
150+
151+
(* ...and the result is numerically correct end-to-end. *)
152+
153+
funBodyDen = MakeFortranFunction[Sin[a] / (mm + a^2) + Cos[a] / (mm + a^2), "Name" -> "funden",
154+
"Body" -> "double precision :: a, mm\na = in1\nmm = in2",
155+
"Parameters" -> {"in1", "in2"}];
156+
157+
(* The hoisted denominator is declared with double precision (required: the
158+
generated function uses implicit none). *)
159+
160+
AppendTo[tests, VerificationTest[
161+
StringContainsQ[funBodyDen, "double precision" ~~ Shortest[__] ~~ "fkden"],
162+
True,
163+
TestID -> "Hoisted denominator gets a double precision declaration in Fortran"
164+
]];
165+
166+
codeDen = funBodyDen <> "
167+
168+
program main
169+
implicit none
170+
double precision :: res, funden
171+
res = funden(1.2d0, 0.7d0)
172+
write(*,'(F25.15)') res
173+
end program main
174+
";
175+
176+
execFileDen = $TemporaryDirectory <> "/FunKitFortranTestDen.f90";
177+
execPathDen = $TemporaryDirectory <> "/FunKitFortranTestDen";
178+
Export[execFileDen, codeDen, "Text"];
179+
180+
compileDen = RunProcess[{"gfortran", "-ffree-form", "-o", execPathDen, execFileDen}];
181+
182+
AppendTo[tests, VerificationTest[compileDen["ExitCode"], 0, TestID -> "Verify compilation of Fortran function with hoisted denominator"]];
183+
184+
outputDen = If[compileDen["ExitCode"] === 0, RunProcess[{execPathDen}], <|"StandardOutput" -> ""|>];
185+
186+
expectedDenVal = N[(Sin[a] / (mm + a^2) + Cos[a] / (mm + a^2)) /. {a -> 1.2, mm -> 0.7}, 15];
187+
fortranDenVal = ToExpression[StringTrim[outputDen["StandardOutput"]]];
188+
189+
AppendTo[tests, VerificationTest[Abs[fortranDenVal - expectedDenVal] < 1*^-8, True, TestID -> "Fortran function with hoisted denominator returns correct value"]];
190+
124191
(**********************************************************************************
125192
Simple expression: plain return, no CSE
126193
**********************************************************************************)

0 commit comments

Comments
 (0)