refactor: pipeline redesign & DIP compliance (Phase 4)
This commit is contained in:
parent
ad85581267
commit
9ee9721c4c
2 changed files with 226 additions and 413 deletions
|
|
@ -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())
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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단계: 수식 내의 미분 및 변동 연산자를 감지하여 해당 물리 필드로 확장 처리합니다.
|
||||
calc_grammar = """
|
||||
?varlist: "[" [NAME ("," NAME)*] "]"
|
||||
|
||||
1단계에서 추출된 변수 식 내부에 ddx(u) 와 같은 수치 미분항이 검출되면, 이를 연산할 중간 미분 필드(DerivedField)를
|
||||
자동 생성하고 의존성 체인에 등록합니다. 또한, u'와 같은 변동 성분이 포함되어 있으면
|
||||
물리량에서 평균치를 뺀 변동량 계산용 임시 필드(FluctuationField)로 확장 생성하여 등록합니다.
|
||||
?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 __init__ (self, src):
|
||||
"""Stage 2 초기화 및 변수 수식 확장을 가동합니다.
|
||||
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)
|
||||
|
||||
Args:
|
||||
src (Stage1): 완료된 Stage 1의 AST 데이터 세트.
|
||||
"""
|
||||
self.src = src
|
||||
self.primary = src.primary
|
||||
self.derived = src.derived
|
||||
self.derivative = {}
|
||||
self.averaged = {}
|
||||
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))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue