refactor: pipeline redesign & DIP compliance (Phase 4)
All checks were successful
CI Test Suite / run-tests (push) Successful in 1m11s
Deploy Documentation / build-and-deploy (push) Successful in 52s

This commit is contained in:
ignis 2026-06-04 08:47:29 +00:00
parent ad85581267
commit 9ee9721c4c
2 changed files with 226 additions and 413 deletions

View file

@ -3,10 +3,16 @@ import sys
import argparse
import datetime
import subprocess as sp
from lark import Lark, Token
from jinja2 import Template
from post import Stage1, Stage2, Stage3, Stage4
from resources.m_template import mod_form
from post import (
CompilationContext,
ParserStage,
DerivativeExpansionStage,
DependencyResolutionStage,
SympyOptimizationStage,
FortranProgramWriter,
ReportWriter,
LatexWriter
)
parser = argparse.ArgumentParser()
parser.add_argument("term_file", help="name of file containing postprocessing term spec")
@ -16,86 +22,6 @@ parser.add_argument("-p", "--print-report", action="store_true", help="print cod
parser.add_argument("-m", "--make-build-module", action="store_true", help="print build_info Fortran module instead of code")
args = parser.parse_args()
calc_grammar = """
?varlist: "[" [NAME ("," NAME)*] "]"
?start: statement*
?statement: NAME [ attr_list ] "=" sum -> assign_var
| avg "{" [NAME ("," NAME)*] "}" -> assign_avg_var
| varlist
attr_list: "(" [attr_pair ("," attr_pair)*] ")"
?attr_pair: NAME "=" BOOL
| NAME "=" INT
| NAME "=" ESCAPED_STRING
?sum: product
| sum "+" product -> add
| sum "-" product -> sub
?product: atom
| product "*" atom -> mul
| product "/" atom -> div
?atom: NUMBER -> number
| "-" atom -> neg
| NAME -> var
| NAME "'" -> fluc
| "$" NAME -> env
| "(" sum ")" -> paren
| inlinefunc "(" sum ")" -> icall
| mathfunc "(" sum ("," sum)* ")" -> fcall
| derivative "(" NAME ")" -> dnx
avg: "avg" [NAME]
?inlinefunc: "sqr" -> sqr
| "pow3" -> pow3
?mathfunc: "log" -> log
| "exp" -> exp
| "sqrt" -> sqrt
| "abs" -> abs
| "rxn_rate" -> rxn_rate
| "$" NAME -> udf
?derivative: "ddx" -> ddx
| "d2dx" -> d2dx
| "ddy" -> ddy
| "d2dy" -> d2dy
| "ddz" -> ddz
| "d2dz" -> d2dz
%import common.CNAME -> NAME
%import common.NUMBER
%import common.ESCAPED_STRING
%import common.INT
%import common.WS
BOOL: "true" | "false"
COMMENT: /#.*/
%ignore COMMENT
%ignore WS
"""
def tok_to_bool(tok):
"Convert the value of `tok` from string to bool, while maintaining line number & column."
# tok.type == 'BOOL'
return Token.new_borrow_pos(tok.type, tok.value == "true", tok)
def tok_to_int(tok):
"Convert the value of `tok` from string to int, while maintaining line number & column."
# tok.type == 'INT'
return Token.new_borrow_pos(tok.type, int(tok), tok)
def tok_to_str(tok):
"Convert the value of `tok` from string to string, while maintaining line number & column."
# tok.type == 'ESCAPED_STRING'
return Token.new_borrow_pos(tok.type, tok.value.strip('"'), tok)
class VersionInfo(object):
@ -137,27 +63,20 @@ def compile_terms(terms_raw):
"""Parses DSL terms spec and runs it through compiler stages 1-4.
Returns:
ir4 (Stage4): The compiled Stage 4 IR object.
ctx (CompilationContext): The compiled context.
"""
parser = Lark(calc_grammar,
parser='lalr',
lexer_callbacks={
'ESCAPED_STRING': tok_to_str,
'INT': tok_to_int,
'BOOL': tok_to_bool
})
tree = parser.parse(terms_raw)
ir1 = Stage1(tree)
ir2 = Stage2(ir1)
ir3 = Stage3(ir2)
ir4 = Stage4(ir3)
return ir4
ctx = CompilationContext()
ParserStage().execute(terms_raw, ctx)
DerivativeExpansionStage().execute(ctx)
DependencyResolutionStage().execute(ctx)
SympyOptimizationStage().execute(ctx)
return ctx
def get_latex_equations_str(ir4):
"""Generates the LaTeX equations dictionary string from Stage4 IR."""
def get_latex_equations_str(ctx):
"""Generates the LaTeX equations dictionary string from the context."""
latex_lines = ["{"]
for avg in ir4.averaged.values():
for avg in ctx.averaged.values():
latex_lines.append(' "{}" : r"${}$",'.format(avg.name, avg.latex))
latex_lines.append("}")
return "\n".join(latex_lines)
@ -166,25 +85,18 @@ def get_latex_equations_str(ir4):
def test(terms_raw, report=False, latex=False):
"""Compiles the post-processing term specifications from DSL into targets.
This compiles the Lark AST through all 4 pipeline stages (Stage 1 to 4)
and outputs the resulting Fortran source code via Jinja2, a LaTeX equation
dictionary, or a code generation IR report.
Args:
terms_raw (str): The raw string contents of the DSL terms specification input file.
report (bool): If True, generates and saves the intermediate code generation report.
latex (bool): If True, prints LaTeX equations of the averaged terms to stdout.
This compiles the Lark AST through all 4 pipeline stages
and outputs the resulting Fortran source code via FortranProgramWriter,
a LaTeX equation dictionary via LatexWriter, or an IR report via ReportWriter.
"""
ir4 = compile_terms(terms_raw)
ctx = compile_terms(terms_raw)
if report:
ir4.save_ir()
ReportWriter().write(ctx)
elif latex:
print(get_latex_equations_str(ir4))
LatexWriter().write(ctx)
else:
cdict = ir4.print_program()
print(Template(mod_form).render(**cdict))
FortranProgramWriter().write(ctx)
@ -203,8 +115,8 @@ def generate_build_info_module(terms_raw):
vinfo = VersionInfo(terms_raw)
build_info_str = vinfo.fparams()
ir4 = compile_terms(terms_raw)
latex_str = get_latex_equations_str(ir4)
ctx = compile_terms(terms_raw)
latex_str = get_latex_equations_str(ctx)
fortran_build_info = escape_fortran_string(build_info_str)
fortran_latex = escape_fortran_string(latex_str)
@ -226,9 +138,7 @@ def build_info(terms_raw):
Args:
terms_raw (str): The raw string contents of the DSL terms specification input file.
"""
vinfo = VersionInfo(terms_raw)
print(vinfo.fparams())

View file

@ -1326,394 +1326,278 @@ class AveragedField (FieldBase):
return len(self.fset) > 0
class Stage1():
"""컴파일러 1단계: DSL 파서 AST 트리로부터 필드 변수 정의 정보를 추출 및 적재합니다.
Lark 구문 파서가 생성한 추상 구문 트리(AST) CollectDefinitions Visitor 클래스로 횡단 스캔하여,
기본 변수(Primary), 수식 유도 변수(Derived), 평균 대상 사양(Averaged) 모아 구조체 딕셔너리로 저장합니다.
"""
def __init__ (self, raw_tree):
"""Stage 1 초기화 및 AST 정의 스캔을 시작합니다.
Args:
raw_tree (lark.Tree): Lark 구문 파서가 해석 완료한 DSL 추상 구문 트리.
"""
self.primary = set([])
class CompilationContext(object):
def __init__(self):
self.primary = set()
self.derived = {}
self.averaged = {}
# CollectDefinitions Visitor를 구동하여 AST 트리 내의 정의 스캔 시작
CollectDefinitions( self.primary, self.derived, self.averaged ).visit(raw_tree)
def __repr__ (self):
return "\n".join(map(str, [self.primary, self.derived, self.averaged]))
self.dependency = {}
self.pass1 = []
self.pass2 = []
self.avg1 = set()
self.avg2 = set()
self.alloc1 = {}
self.alloc2 = {}
self.narr = 0
class Stage2():
"""컴파일러 2단계: 수식 내의 미분 및 변동 연산자를 감지하여 해당 물리 필드로 확장 처리합니다.
1단계에서 추출된 변수 내부에 ddx(u) 같은 수치 미분항이 검출되면, 이를 연산할 중간 미분 필드(DerivedField)
자동 생성하고 의존성 체인에 등록합니다. 또한, u'와 같은 변동 성분이 포함되어 있으면
물리량에서 평균치를 변동량 계산용 임시 필드(FluctuationField) 확장 생성하여 등록합니다.
"""
calc_grammar = """
?varlist: "[" [NAME ("," NAME)*] "]"
def __init__ (self, src):
"""Stage 2 초기화 및 변수 수식 확장을 가동합니다.
?start: statement*
Args:
src (Stage1): 완료된 Stage 1 AST 데이터 세트.
"""
self.src = src
self.primary = src.primary
self.derived = src.derived
self.derivative = {}
self.averaged = {}
?statement: NAME [ attr_list ] "=" sum -> assign_var
| avg "{" [NAME ("," NAME)*] "}" -> assign_avg_var
| varlist
attr_list: "(" [attr_pair ("," attr_pair)*] ")"
?attr_pair: NAME "=" BOOL
| NAME "=" INT
| NAME "=" ESCAPED_STRING
?sum: product
| sum "+" product -> add
| sum "-" product -> sub
?product: atom
| product "*" atom -> mul
| product "/" atom -> div
?atom: NUMBER -> number
| "-" atom -> neg
| NAME -> var
| NAME "'" -> fluc
| "$" NAME -> env
| "(" sum ")" -> paren
| inlinefunc "(" sum ")" -> icall
| mathfunc "(" sum ("," sum)* ")" -> fcall
| derivative "(" NAME ")" -> dnx
avg: "avg" [NAME]
?inlinefunc: "sqr" -> sqr
| "pow3" -> pow3
?mathfunc: "log" -> log
| "exp" -> exp
| "sqrt" -> sqrt
| "abs" -> abs
| "rxn_rate" -> rxn_rate
| "$" NAME -> udf
?derivative: "ddx" -> ddx
| "d2dx" -> d2dx
| "ddy" -> ddy
| "d2dy" -> d2dy
| "ddz" -> ddz
| "d2dz" -> d2dz
%import common.CNAME -> NAME
%import common.NUMBER
%import common.ESCAPED_STRING
%import common.INT
%import common.WS
BOOL: "true" | "false"
COMMENT: /#.*/
%ignore COMMENT
%ignore WS
"""
def tok_to_bool(tok):
"Convert the value of `tok` from string to bool, while maintaining line number & column."
return Token.new_borrow_pos(tok.type, tok.value == "true", tok)
def tok_to_int(tok):
"Convert the value of `tok` from string to int, while maintaining line number & column."
return Token.new_borrow_pos(tok.type, int(tok), tok)
def tok_to_str(tok):
"Convert the value of `tok` from string to string, while maintaining line number & column."
return Token.new_borrow_pos(tok.type, tok.value.strip('"'), tok)
class ParserStage(object):
def __init__(self):
self.parser = Lark(calc_grammar,
parser='lalr',
lexer_callbacks={
'ESCAPED_STRING': tok_to_str,
'INT': tok_to_int,
'BOOL': tok_to_bool
})
def execute(self, terms_raw, ctx):
tree = self.parser.parse(terms_raw)
CollectDefinitions(ctx.primary, ctx.derived, ctx.averaged).visit(tree)
class DerivativeExpansionStage(object):
def execute(self, ctx):
# 1. 계산식 내에 존재하는 고차 차분 미분항(ddx, d2dy 등)을 찾아 중간 DerivedField로 등록
dset = set([])
for k, v in self.derived.items():
dset = set()
for k, v in ctx.derived.items():
dset.update(v.derivs)
for tup in dset:
a = DerivedField(tup[0], tup[1], self.derived)
self.derived[a.name] = a
self.derivative[tup] = a
a = DerivedField(tup[0], tup[1], ctx.derived)
ctx.derived[a.name] = a
# 2. 통계 물리량 평균화 대상 변수들을 AveragedField 구조체로 구성하고,
# 변동량 계산이 필요한 경우 FluctuationField로 등록
for w, tgts in src.averaged.items():
averaged_raw = ctx.averaged
ctx.averaged = {}
for w, tgts in averaged_raw.items():
for t in tgts:
a = AveragedField(w, t, self.derived)
self.averaged[a.name] = a
a = AveragedField(w, t, ctx.derived)
ctx.averaged[a.name] = a
# 평균 편차가 동반된 항들에 대해 FluctuationField 생성
for ff in a.fset:
b = FluctuationField(w, ff, a.fset, self.derived)
self.derived[b.name] = b
b = FluctuationField(w, ff, a.fset, ctx.derived)
ctx.derived[b.name] = b
def __repr__ (self):
return "\n".join(map(str, [self.derived, self.derivative, self.averaged]))
def dependency (self):
"""프로그램 내 모든 필드 간의 1차 의존 관계 그래프(Dependency Graph)를 추출하여 딕셔너리로 반환합니다."""
dgraph = {}
for k,v in self.derived.items():
dgraph[k] = v.dep
for k,v in self.averaged.items():
dgraph[k] = v.dep
return dgraph
# 3. 프로그램 내 모든 필드 간의 1차 의존 관계 그래프(Dependency Graph) 추출
ctx.dependency = {}
for k, v in ctx.derived.items():
ctx.dependency[k] = v.dep
for k, v in ctx.averaged.items():
ctx.dependency[k] = v.dep
class Stage3():
"""컴파일러 3단계: 연산 간의 의존성을 해결하여 3D 계산 루프의 최적 실행 순서를 계산합니다.
1. 루프 분할(Loop Splitting): 난류 평균량 연산의 인과관계에 따라 물리 연산을 개의 단계(Pass) 쪼갭니다.
- Pass 1 (평균 계산 ): 입력 물리량들로부터 1 수식을 연산하고, 1 평균값(: `<u_w>`) 누적합하는 연산들.
- Pass 2 (평균 계산 ): 구해진 평균값을 활용하여 변동량(u' = u - `<u_w>`)을 계산하고,
이를 통반한 최종 결합 물리량을 추가적으로 계산/평균화하는 연산들.
2. 최적화 위상 정렬(Topological Sort): Pass 내에서 변수의 선행 의존 관계가 완전히 해결된
올바른 순서로 순차 연산 코드가 생성되도록 정렬 알고리즘을 가동합니다.
"""
def __init__ (self, src):
"""Stage 3 초기화 및 위상 정렬을 수행하여 Pass 1, 2 실행 순서를 결정합니다.
Args:
src (Stage2): 확장된 변수 테이블을 가진 Stage 2 결과 객체.
"""
self.src = src
self.primary = src.primary
self.derived = src.derived
self.averaged = src.averaged
self.dependency = src.dependency()
class DependencyResolutionStage(object):
def execute(self, ctx):
# 이름 충돌 방지 검증
assert set(self.derived.keys()).isdisjoint(self.averaged.keys())
assert set(ctx.derived.keys()).isdisjoint(ctx.averaged.keys())
# Pass 1과 Pass 2 대상 평균화 변수 논리 분할
self.avg1 = set(filter(AveragedField.pass1, self.averaged.values()))
self.avg2 = set(filter(AveragedField.pass2, self.averaged.values()))
ctx.avg1 = set(filter(AveragedField.pass1, ctx.averaged.values()))
ctx.avg2 = set(filter(AveragedField.pass2, ctx.averaged.values()))
# Pass 1 위상 정렬: Pass 1 평균 변수들의 연산에 관여하는 모든 종속 관계를 수집하여 정렬
pass1calc = set(map(repr, self.avg1))
for x in self.avg1:
pass1calc = set(map(repr, ctx.avg1))
for x in ctx.avg1:
pass1calc.update(x.depClosure())
self.pass1 = self.sort_vars_new(self.dependency, pass1calc - self.primary)
ctx.pass1 = self.sort_vars_new(ctx.dependency, pass1calc - ctx.primary)
# Pass 2 위상 정렬: Pass 2 평균 변수(변동 연산 연계)들의 연산에 관여하는 종속성을 정렬
pass2calc = set(map(repr, self.avg2))
for x in self.avg2:
pass2calc = set(map(repr, ctx.avg2))
for x in ctx.avg2:
pass2calc.update(x.depClosure())
self.pass2 = self.sort_vars_new(self.dependency, pass2calc - self.primary)
ctx.pass2 = self.sort_vars_new(ctx.dependency, pass2calc - ctx.primary)
def __repr__ (self):
return "\n".join(map(str, [self.pass1, self.pass2]))
def sort_vars (self, dependency, group):
"""일반적인 단순 위상 정렬 알고리즘입니다."""
order = []
remain = list(group)
remain.sort()
while len(remain) > 0:
for v in remain:
if dependency[v].isdisjoint(remain):
order.append(v)
remain.remove(v)
return order
def calc_size (self, ordered, remaining):
"""위상 정렬 도중 활성화되는 임시 라이브 변수들의 상대적 가중치 스케일을 계산합니다.
메모리 상주 대역을 최소화하는 최적의 정렬 후보를 결정할 척도로 이용됩니다.
"""
def calc_size (self, dependency, ordered, remaining):
count = 0
dep_union = set([])
g = self.dependency
dep_union = set()
for v in remaining:
dep_union |= set(g[v])
dep_union |= set(dependency[v])
for v in ordered:
if v in dep_union:
count += 1
return count
def sort_vars_new (self, dependency, group):
"""메모리에 동시에 상주하여 참조되어야 하는 활성 라이브 변수의 개수를
최소화하도록 특수 설계된 메모리 인지형 위상 정렬(Memory-Aware Topological Sort) 알고리즘입니다.
정렬 단계마다 현재까지 결정된 실행 흐름(ordered) 향후 연산되어야 대기 변수(remaining) 사이의
의존도 영향 범위(calc_size) 계산하여, 생명 주기 (Liveness Window) 짧아지도록 후보군 최선의 변수를
선택해 나가며 실행 순서 목록을 빌드합니다.
"""
order = []
remain = list(group)
remain.sort()
while len(remain) > 0:
candidate = []
for v in remain:
if set(dependency[v]).isdisjoint(remain):
candidate.append(v)
impact = {}
size0 = self.calc_size(set(order), set(remain))
size0 = self.calc_size(dependency, set(order), set(remain))
for v in candidate:
impact[v] = self.calc_size(set(order) | set([v]), set(remain) - set([v])) - size0
impact[v] = self.calc_size(dependency, set(order) | set([v]), set(remain) - set([v])) - size0
candidate.sort(key=impact.get)
# 활성 변수 상주 가중치 영향도가 가장 적은 후보를 우선 배치
order.append(candidate[0])
remain.remove(candidate[0])
return order
def print_program (self):
"""Stage 3 결과를 바탕으로, 최적화가 적용되지 않은 평이한 단일 3D 배열식으로
모든 중간 임시 변수를 개별 동적 할당하여 계산하는 원시 Fortran 소스코드를 인쇄합니다.
"""
allvar = dict(self.derived)
allvar.update(self.averaged)
generator = FortranCodeGenerator(allvar)
decl = "\n".join(generator.generate_decl(allvar[v]) for v in set(self.pass1+self.pass2))
alloc = "\n".join(generator.generate_alloc(allvar[v]) for v in set(self.pass1+self.pass2))
free = "\n".join(generator.generate_free(allvar[v]) for v in set(self.pass1+self.pass2))
calc1 = "\n".join(generator.generate_code(allvar[v]) for v in self.pass1)
calc2 = "\n".join(generator.generate_code(allvar[v]) for v in self.pass2)
set1 = sorted([a.name for a in filter(AveragedField.pass1, self.averaged.values())])
set2 = sorted([a.name for a in filter(AveragedField.pass2, self.averaged.values())])
avg1 = "\n".join(generator.generate_avg(allvar[v]) for v in set1)
avg2 = "\n".join(generator.generate_avg(allvar[v]) for v in set2)
hfmt = 'character (len = *), parameter :: output_header="{}"'
declh = hfmt.format(" ".join(["x"] + set1 + set2))
write_avg = generator.generate_write_avg(set1+set2)
md = {}
md["module_name"] = "terms"
md["module_data"] = "\n".join((declh, decl))
md["module_init"] = alloc
md["module_finalize"] = free
md["module_pass1"] = calc1
md["module_pass1_avg"] = avg1
md["module_pass2"] = calc2
md["module_pass2_avg"] = avg2
md["module_write_result"] = write_avg
return md
class Stage4():
"""컴파일러 4단계 (최종): 메모리 Pooling 버퍼 할당 및 SymPy 최적화 코드를 최종 결합하여 컴파일을 완료합니다.
[핵심 최적화 기술]
1. SymPy 수식 정리 의존 기호 교정:
- SymPy 기호 엔진을 통해 최적화 대상 필드들의 수식 트리를 전면 간소화합니다.
- 기호화 과정에서 대입 치환(substitution)으로 바뀐 수식들의 의존성을 재추적하여
컴파일러의 dependency 그래프를 정확한 상태로 최신화합니다.
2. 생명 주기 분석 (Liveness Analysis):
- 3D 배열은 크기가 막대하여 루프 중간에 생성되는 수십 개의 임시 변수들에게 각자 별도 배열을 할당하면
프로그램 실행이 불가능해집니다.
- 임시 변수가 할당되어 계산되는 시작점과 변수를 다른 수식들이 완전히 가져다
종료되는 참조 종점(Liveness window) 2차원 부울 마스크 이미지 형태로 추적합니다.
3. 메모리 Pooling 버퍼 할당 (Array Register Allocation):
- Liveness 마스크 분석을 통해 동일 시점에 공존하여 메모리에 살아있어야 하는 임시 변수의 최대 중첩 개수를 파악하여
필요한 버퍼의 총수(self.narr) 계산합니다.
- 재사용 가능한 버퍼 명칭 (pool) 정의하고, Liveness에 의해 변수의 생명이 다하면 사용하던 버퍼명을 풀에 반환하며,
새로운 변수가 활성화될 풀에서 버퍼명을 분배(Pop)하는 방식으로 3차원 물리 필드 메모리 오버헤드를
O(1) 수준의 버퍼 세트로 극도로 낮춥니다 (xyzbuffer0, xyzbuffer1 ).
"""
def __init__ (self, src):
self.src = src
self.primary = src.primary
self.derived = src.derived
self.averaged = src.averaged
self.dependency = src.dependency
self.avg1 = src.avg1
self.avg2 = src.avg2
self.pass1 = src.pass1
self.pass2 = src.pass2
class SympyOptimizationStage(object):
def execute(self, ctx):
# 1. SymPy 수식 최적화 엔진 초기화
opt = SympyOptimizer.get_instance(self.derived)
opt.set_averaged(self.averaged)
opt = SympyOptimizer.get_instance(ctx.derived)
opt.set_averaged(ctx.averaged)
# 2. SymPy가 대입식 치환(Substitution) 과정에서 제거한 불필요한 의존성 관계를
# 의존성 그래프에 즉각 반영하여 실제 계산을 위한 의존성 체인을 슬림하게 정리합니다.
updated_dependency = {}
for name, dep_set in self.dependency.items():
if name in self.derived and isinstance(self.derived[name], Field):
for name, dep_set in ctx.dependency.items():
if name in ctx.derived and isinstance(ctx.derived[name], Field):
expr = opt.get_sympy_expr(name)
# 실제 정리된 SymPy 식에 잔존하는 자유 기호 명칭들만 추출
free_sym_names = {sym.name for sym in expr.free_symbols}
valid_deps = {dep for dep in free_sym_names if dep in self.derived or dep in self.primary}
valid_deps = {dep for dep in free_sym_names if dep in ctx.derived or dep in ctx.primary}
updated_dependency[name] = valid_deps
else:
updated_dependency[name] = dep_set
self.dependency = updated_dependency
ctx.dependency = updated_dependency
self.array_name = "xyzbuffer{}" # Pooling용 공용 3차원 버퍼 이름 포맷
# 3. Pass 1 및 Pass 2 연산 순서 배열들에 대해 각각 버퍼 공유 매핑(Pooling) 수행
narr1, alloc1 = (self.allocate_arr(self.pass1))
narr2, alloc2 = (self.allocate_arr(self.pass2))
narr1, alloc1 = (self.allocate_arr(ctx, ctx.pass1))
narr2, alloc2 = (self.allocate_arr(ctx, ctx.pass2))
# 전체 프로그램에서 필요한 동적 공유 3D 버퍼 배열의 최대 크기 설정
self.narr = max(narr1, narr2)
ctx.narr = max(narr1, narr2)
self.alloc1 = alloc1 # Pass 1 변수명 -> 공용 xyzbuffer 명칭 매핑 정보
self.alloc2 = alloc2 # Pass 2 변수명 -> 공용 xyzbuffer 명칭 매핑 정보
ctx.alloc1 = alloc1 # Pass 1 변수명 -> 공용 xyzbuffer 명칭 매핑 정보
ctx.alloc2 = alloc2 # Pass 2 변수명 -> 공용 xyzbuffer 명칭 매핑 정보
def liveness (self, l1, g):
"""위상 정렬된 변수 시퀀스 `l1`과 의존성 그래프 `g`를 이용하여
변수가 루프 타임라인 내내 살아있어야 하는 구간(Liveness window) 2D 부울 행렬로 구성합니다.
행렬의 (i, j) True이면, i번째 변수가 j번째 연산 시점에 살아있어야 함을 의미합니다.
"""
def liveness (self, ctx, l1, g):
import numpy as np
img = np.zeros((len(l1), len(l1)))
for i, v in enumerate(l1):
for j in range(i, len(l1)):
# 만약 i번째 변수가 훗날 j번째 변수를 계산할 때의 하위 의존성(g[l1[j]])에 포함되어 있으면,
# i번째 변수는 j번째 연산 시점까지 생명이 연장되어 살아있어야(live) 합니다.
img[i,i:j+1] = img[i,i:j+1] + (1 if v in g[l1[j]] else 0)
return img > 0
def allocate_arr (self, l):
"""Liveness 분석 데이터를 바탕으로 동적 3D 배열 레지스터 할당(Pooling)을 수행합니다.
동시 활성 상태를 파악하여 최소 버퍼 개수(narr) 도출하고, 변수 버퍼 공유 관계(var2arr) 리턴합니다.
"""
def allocate_arr (self, ctx, l):
import numpy as np
dg = self.dependency
# 1. 생명 주기 마스크 행렬 획득
mask = self.liveness(l, dg)
# 2. 열 방향 합계를 내어 동시에 생존해야 하는 임시 변수의 최대 개수(동시 상주 배열 피크치)를 산출
dg = ctx.dependency
mask = self.liveness(ctx, l, dg)
try:
narr = mask.astype(int).sum(axis=0).max()
except ValueError:
narr = 0
# 사용 가능한 공유 버퍼 풀 정의
array_pool = set([self.array_name.format(i) for i in range(narr)])
# 각 계산 단계별로 살아있는 변수들의 집합 구성
livesets = [set([])] + [set(np.asarray(l)[row]) for row in mask.T]
var2arr = { p : p for p in ctx.primary }
# 기본 입력 변수들은 파일로부터 직접 매핑되므로 원래 명칭 유지
var2arr = { p : p for p in self.primary }
# 루프 연산 순서 타임라인을 한 단계씩 밟아가며 동적 버퍼 매핑 시뮬레이션 수행
for i, (s0, s1) in enumerate(zip(livesets[:-1], livesets[1:])):
# 이전 단계(s0)에는 살아있었으나 이번 단계(s1)에서는 죽은 변수들(s0 - s1)의
# 할당 버퍼를 다시 사용 가능한 array_pool로 회수(반환)합니다.
array_pool.update(map(var2arr.get, s0 - s1))
# 이번 단계에 새로 생성되는 변수들(s1 - s0)에게 풀에서 노는 버퍼명을 Pop하여 할당(배분)합니다.
for new in s1 - s0:
var2arr[new] = array_pool.pop()
return narr, var2arr
def work_array_codes (self):
"""생성된 Fortran 모듈 상단에 선언 및 할당/해제될 공용 3D 버퍼 배열(xyzbuffer0, ...)의
선언문, allocate문, deallocate문 코드를 작성합니다.
"""
array_names = [self.array_name.format(i) for i in range(self.narr)]
class FortranProgramWriter(object):
def write(self, ctx):
from resources.m_template import mod_form
real_array_decl = "real(real64), allocatable, dimension(:,:,:) :: {0}"
decl = "\n".join([real_array_decl.format(v) for v in array_names])
alloc = "\n".join([make_allocate(v, "nxp,nyp,nzp") for v in array_names])
free = "\n".join(["deallocate({})".format(v) for v in array_names])
return decl, alloc, free
def print_program (self):
"""4단계 최적화(공유 Pooling 버퍼 매핑 및 SymPy CSE 치환)가 완료된
가장 고성능의 최종 Fortran 모듈 코드를 생성하기 위해 Jinja2용 딕셔너리를 빌드합니다.
"""
opt = SympyOptimizer.get_instance(self.derived)
opt.set_averaged(self.averaged)
allvar = dict(self.derived)
allvar.update(self.averaged)
allvar = dict(ctx.derived)
allvar.update(ctx.averaged)
generator = FortranCodeGenerator(allvar)
# 평균 변수들을 순서대로 분배
set1 = sorted([a.name for a in filter(AveragedField.pass1, self.averaged.values())])
set2 = sorted([a.name for a in filter(AveragedField.pass2, self.averaged.values())])
set1 = sorted([a.name for a in filter(AveragedField.pass1, ctx.averaged.values())])
set2 = sorted([a.name for a in filter(AveragedField.pass2, ctx.averaged.values())])
# 외부 디스크 파일 익스포트 활성화 여부 확인
set_export_on = list(filter(lambda x: x.export_on(), self.derived.values()))
set_export_on = list(filter(lambda x: x.export_on(), ctx.derived.values()))
ffmt = 'logical, parameter :: pass2_required={}'
declf = ffmt.format('.true.' if len(set2) > 0 else '.false.')
@ -1722,12 +1606,12 @@ class Stage4():
declh = hfmt.format(" ".join(["x"] + set1 + set2))
# 공용 Pooling 3D 버퍼 배열의 선언/할당 코드 생성
declarr, allocarr, freearr = self.work_array_codes()
declarr, allocarr, freearr = self.work_array_codes(ctx.narr)
# 1차원 평균 물리량 배열들의 선언/할당 코드 생성
declavg = "\n".join(generator.generate_decl(self.averaged[v]) for v in sorted(self.averaged))
allocavg = "\n".join(generator.generate_alloc(self.averaged[v]) for v in sorted(self.averaged))
freeavg = "\n".join(generator.generate_free(self.averaged[v]) for v in sorted(self.averaged))
declavg = "\n".join(generator.generate_decl(ctx.averaged[v]) for v in sorted(ctx.averaged))
allocavg = "\n".join(generator.generate_alloc(ctx.averaged[v]) for v in sorted(ctx.averaged))
freeavg = "\n".join(generator.generate_free(ctx.averaged[v]) for v in sorted(ctx.averaged))
# 병렬 파일 쓰기(MPI Subarray)를 위한 MPI 리소스 선언/할당 코드 생성
decl_export = "\n".join(generator.generate_decl(v.exporter) for v in set_export_on)
@ -1735,8 +1619,8 @@ class Stage4():
free_export = "\n".join(generator.generate_free(v.exporter) for v in set_export_on)
# Pass 1과 Pass 2 루프 내부 본문 연산 코드들을 생성 (각자 버퍼 맵 alloc1, alloc2 적용)
sub_calc1 = "\n".join(generator.generate_code(allvar[v], self.alloc1) for v in self.pass1 if v in self.averaged or v in self.alloc1)
sub_calc2 = "\n".join(generator.generate_code(allvar[v], self.alloc2) for v in self.pass2 if v in self.averaged or v in self.alloc2)
sub_calc1 = "\n".join(generator.generate_code(allvar[v], ctx.alloc1) for v in ctx.pass1 if v in ctx.averaged or v in ctx.alloc1)
sub_calc2 = "\n".join(generator.generate_code(allvar[v], ctx.alloc2) for v in ctx.pass2 if v in ctx.averaged or v in ctx.alloc2)
# 평균 누적 연산 코드 생성
sub_avg1 = "\n".join(generator.generate_avg(allvar[v]) for v in set1)
@ -1756,18 +1640,37 @@ class Stage4():
md["module_pass2_avg"] = sub_avg2
md["module_write_result"] = sub_write_avg
return md
print(Template(mod_form).render(**md))
def work_array_codes(self, narr):
array_name = "xyzbuffer{}"
array_names = [array_name.format(i) for i in range(narr)]
real_array_decl = "real(real64), allocatable, dimension(:,:,:) :: {0}"
decl = "\n".join([real_array_decl.format(v) for v in array_names])
alloc = "\n".join([make_allocate(v, "nxp,nyp,nzp") for v in array_names])
free = "\n".join(["deallocate({})".format(v) for v in array_names])
return decl, alloc, free
def save_ir (self):
"""코드 생성 디버깅을 위해 중간 컴파일 단계 변수 목록 및 의존성 맵을 JSON 형태 파일로 덤프합니다."""
class ReportWriter(object):
def write(self, ctx):
import json
dg = {k:list(v) for k,v in self.dependency.items()}
dg = {k:list(v) for k,v in ctx.dependency.items()}
with open("ir2.py", "w") as irf:
print("g = ", json.dumps(dg, indent=4), file=irf)
print("l1 = ", json.dumps(self.pass1, indent=4), file=irf)
print("l2 = ", json.dumps(self.pass2, indent=4), file=irf)
print("avg1 = ", json.dumps(list(map(repr,self.avg1)), indent=4), file=irf)
print("avg2 = ", json.dumps(list(map(repr,self.avg2)), indent=4), file=irf)
print("l1 = ", json.dumps(ctx.pass1, indent=4), file=irf)
print("l2 = ", json.dumps(ctx.pass2, indent=4), file=irf)
print("avg1 = ", json.dumps(list(map(repr, ctx.avg1)), indent=4), file=irf)
print("avg2 = ", json.dumps(list(map(repr, ctx.avg2)), indent=4), file=irf)
class LatexWriter(object):
def write(self, ctx):
latex_lines = ["{"]
for avg in ctx.averaged.values():
latex_lines.append(' "{}" : r"${}$",'.format(avg.name, avg.latex))
latex_lines.append("}")
print("\n".join(latex_lines))