0. 이 시스템이 지금 “성립”하는 핵심 한 줄
AICF v2에서 학습 CUDA Graph replay가 가능한 이유는 딱 3개가 동시에 맞물리기 때문임:
- externals(input/param/state) 전부를 내부 고정 버퍼(ext_bufs)로 재배치해서 CUDA Graph가 잡는 포인터를 고정한다.
- ExecPlan.alias가 SSA 출력(outP 등)을 입력(P 등) 버퍼로 alias해 커널이 사실상 in-place 업데이트를 하게 만든다.
- train 모드에서 replay 시 input만 copy하고 param/state는 copy하지 않아, 업데이트가 내부 버퍼에 누적된다.
이 3개 중 하나라도 깨지면 “replay 학습”은 깨진다.
1. 전체 구조(파일/모듈 레벨) 맵
1.1 핵심 모듈
- aicf_v2/model.py
- Model: 파라미터/상태 실텐서 저장소 + tape 기반 autodiff + Builder 조립
- Sequential: build/compile/run의 사용자 API
- aicf_v2/builder.py
- Builder: Value/Op 저장 (vid 기반 IR)
- aicf_v2/compile/compile.py
- compile_cuda(m): optimize_ir → make_exec_plan_cuda → CompiledProgram(plan)
- aicf_v2/compile/plan.py
- make_exec_plan_cuda(b): alias 결정(현재는 constraints 기반 + adam_step special-case)
- aicf_v2/runtime/cuda_exec.py
- CudaExecutor: compile_cached / capture_prebuilt / run_compiled
- aicf_v2/runtime/graph_capture.py
- capture_cuda_graph: 고정 포인터 버퍼 기반 캡처
- replay_cuda_graph: input copy + graph.replay
- aicf_v2/layers/*, aicf_v2/emitters/cuda/*
- 레이어는 emit()/emit_backward()로 IR 생성
- emitter는 emit_resolved()로 kind_id/schema/blob 포함 op를 Builder에 기록
2. 기본 데이터 모델: Value/Op/vid/slot
2.1 vid는 “Value 인덱스”이자 “slot 인덱스”
Builder._new_value():
- vid = len(self.values)
- self.values.append(Value(vid=vid,...))
런타임에서는:
- eager path: slots: Dict[int, Tensor]에서 vid 키로 사용
- graph path: slots: List[Tensor]에서 slots[vid]로 사용
즉 vid는 단순 ID가 아니라 메모리 슬롯 주소 지정자다.
불변조건(invariant)
- len(slots) == len(b.values)가 성립해야 한다.
- Value가 추가되면 vid는 증가하며, 기존 vid의 의미는 변하지 않는다.
실패 모드
- “optimize_ir”에서 Builder를 새로 만들면서 vid를 재번호(reindex)하면, 외부에 저장된 vid(예: 사용자가 get_vid_by_name 같은 걸로 보관한 것)가 깨질 수 있다.
- 현재 optimize_ir는 identity라 안전하지만, 추후 패스 추가 시 매우 위험해질 수 있다.
개선안
- (강력 추천) “vid 안정성”을 깨는 최적화(재번호)는 Compile 이후 외부로 vid를 노출하지 않는 방향으로 제한.
- 사용자/테스트가 슬롯 접근할 때는 vid 직접 노출 대신 name→vid 매핑 API를 공식화:
- Model.vid(name), Program.tensor(name) 같은 안정 API 제공
- optimize_ir가 Builder를 새로 만들 필요가 생기면:
- old_vid -> new_vid remap 테이블을 같이 제공하거나,
- 외부에서 vid를 절대 보관하지 않도록 시스템적으로 막는 게 맞다.
2.2 Value role: input / param / state / tmp
Builder는 externals를 role로 분리한다:
- input: 순수 입력(x, y_true, grad_initial, bc1/bc2 같은 외부 주입)
- param: 학습 파라미터(W, b)
- state: 옵티마이저/상태(m, v, step 등)
- tmp: 내부 중간 텐서(activation, grad intermediate 등)
Builder는 또한:
- external_vids = input + param + state를 모아둔다.
불변조건
- CUDA Graph 캡처 모드에서는 external_vids 전부가 고정 포인터 버퍼를 가져야 한다.
- graph_capture는 이를 강제한다(아래 6장에서 설명).
3. 빌드 단계: Layer.emit으로 IR 생성 + tape 기록
3.1 Model.add(layer, *args)의 역할
Model.add()는 2가지 일을 동시에 한다.
- IR 생성
- out_vids = layer.emit(self.b, *args, ctx=self.ctx, ...)
- 자동미분을 위한 forward 기록(tape)
- "inputs": list(args)
- "outputs": [out_vids] 또는 list
- "params": new_params (emit 도중 새로 추가된 param vids)
그리고 추가로 “자동 파라미터 바인딩”도 한다:
- 레이어가 b.param("fc1.W") 같은 IR 등록만 하면
- Model이 실제 torch.Tensor를 만들어 self.parameters[name]=tensor로 저장한다.
불변조건
- 레이어 emit은 Builder에 Value/Op를 “append-only”로 추가해야 한다.
- b.param_vids는 파라미터 Value들을 포함하며, 동일 이름이면 재사용한다.
실패 모드
- 레이어가 실수로 같은 이름의 param을 중복 생성하면 _new_value가 자동으로 suffix를 붙인다.
- 그러면 name 기반 디버깅에서 “fc1.W”가 아닌 “fc1.W_123”이 생겨서 찾기 어려워짐.
개선안
- param/state/input은 _name2vid에서 강하게 중복 방지하고(현재 param/state는 name 있으면 기존 vid 반환이라 OK),
- tmp value도 이름 중복을 허용하되 “디버깅용 라벨”과 “키”를 분리:
- key는 내부적으로 unique id
- name은 display label
- 또는 tmp는 아예 name을 필수로 받지 않고 자동 생성 규칙을 통일(“opname:out0@opidx” 같은 형태)
3.2 Sequential.build 흐름
Sequential.build(input_spec, input_name="x")
- curr_vid = self.input("x", spec)
- 레이어 순회:
- curr_vid = self.add(layer, curr_vid)
- self.output("output", curr_vid)
의미
- build는 “실행 가능한 최종 결과”를 b.outputs["output"]=vid로 고정해둔다.
- 이 output mapping은 replay 시 _extract_outputs에서 그대로 사용된다.
4. Loss & Backward: tape 역순 자동미분 생성
4.1 seed gradient: grad_initial은 input role로 추가됨
build_backward(loss_vid)는:
- loss의 spec을 가져와
- self.b.input("grad_initial", loss_spec)를 만든다.
- grad_map = {loss_vid: grad_initial_vid}
즉 grad_initial은 “외부 feed로 들어오는 값”으로 취급된다.
중요한 점
- train 모드에서 copy_roles=("input",)이므로 grad_initial은 replay마다 바뀔 수 있다(복사 대상).
- 지금 테스트에서 grad_initial을 feed로 주는 이유가 정확히 이 설계 때문.
4.2 tape 역순 순회 규칙
현재 코드 핵심은:
outs = entry["outputs"]
if not outs or outs[0] not in grad_map:
continue
grad_y = grad_map[outs[0]]
즉 “첫 output에 대한 grad가 존재할 때만” backward 확장함.
실패 모드
- multi-output op/layer에서 outs[0] 외 output에 대한 grad가 들어오는 케이스를 다루지 못함.
- branching 그래프에서는 동일 input에 대한 grad가 여러 경로로 모여야 하는데, 지금은 overwrite만 한다.
개선안(필수급)
- grad_map을 vid -> grad_vid로 유지하되, 이미 grad가 있으면 Add로 누적:
- grad_map[x] = add(grad_map[x], new_grad)
- entry의 outputs 전체에 대해 grad를 처리하도록 확장:
- 최소: “grad가 존재하는 output들”을 모아 layer.emit_backward로 넘김
- Layer.emit_backward 표준 계약을 강화:
- 입력 grads를 dict로 반환: { "inputs": [dx0, dx1...], "params": {...} }
- 혹은 {"input_grads": {vid: grad_vid}} 방식
4.3 parameter_grads 생성
layer.emit_backward()가 반환한 dict에서:
- "weight"/"bias" 키가 있으면 parameter_grads[param_vid] = grad_vid
이건 optimizer가 “파라미터에 대한 grad vid를 찾아 step op를 만든다”는 구조의 기반이다.
개선안
- 지금은 param이 2개(weight,bias)라는 전제를 깔고 있음.
- 일반화하려면 “params list와 grads dict를 name/vid 기반으로 매핑”해야 한다.
- 예: layer_grads["params"] = {param_vid: grad_vid, ...}
5. Optimizer: AdamStep는 SSA out을 만든다 → alias로 접는다
5.1 AdamStep 레이어
AdamStep.emit:
- Pout/Mout/Vout를 b.value()로 새 Value로 만든다(SSA 스타일)
- emitter adam_step를 호출하면서:
- inputs: [P, G, M, V, bc1, bc2]
- outputs: [Pout, Mout, Vout]
- constraints: {"inplace_ok": True}
- hints: view_rank0_inputs=[4,5] (bc1/bc2 스칼라 ABI fix)
핵심 의미
- IR 상으로는 “새 텐서가 나오는 함수형 업데이트”처럼 모델링한다.
- 하지만 실행에서는 in-place를 허용한다는 힌트를 constraints로 준다.
5.2 ABI fixups: bc1/bc2 (1,) → rank0 view
runtime의 _apply_abi_fixups()가:
- hints에 view_rank0_inputs가 있으면,
- dim==1 & numel==1 텐서를 .view(())로 바꿔 op_call에 전달한다.
장점
- v2에서 “0-dim spec을 금지”해도 ABI는 만족 가능.
개선안
- 장기적으로는 TensorSpec에 “scalar” 표현을 공식화하는 게 깔끔하다.
- spec.shape=()를 허용하든,
- 또는 dtype/shape 외에 “rank0 semantic scalar” 플래그를 두든
6. compile_cuda: optimize_ir → plan(alias) → CompiledProgram
6.1 compile_cuda 코드 그대로 의미 해설
compile_cuda(m):
- b0 = m.b
- b1 = optimize_ir(b0) (현재 identity)
- plan = make_exec_plan_cuda(b1)
- return CompiledProgram(plan=plan)
현실
- 지금은 “IR 최적화도 lowering도 없다”.
- 따라서 plan은 b.ops 그대로를 실행 순서로 삼는다.
- alias만 결정한다.
개선안(로드맵)
- optimize_ir에 들어갈 후보:
- fuse: gemm + bias_add → gemm_epilogue
- DCE: 쓰이지 않는 tmp 제거
- CSE: 동일 계산 재사용
- layout canonicalization (contiguous 강제 등)
- lower 단계(중요):
- op.kind string을 backend kind_id/schema/blob로 채우는 작업은 지금 emitter가 하고 있음.
- 하지만 장기적으로는 high-level kind → backend variant 선택이 필요해짐.
- 그때 registry가 compile 단계에서 의미를 갖게 된다.
7. plan(make_exec_plan_cuda): SSA를 in-place로 접는 핵심 단계
7.1 현재 alias 정책(코드 그대로)
- constraints["inplace_ok"] == True인 op만 고려
- 특수 케이스:
- adam_step: outputs 3개를 각각 inputs P/M/V에 alias
- 단, Pout == G 금지
- 일반 케이스:
- _ALIAS_OUT0_IN0 whitelist(kind 기반)에 속하면 out0 -> in0 alias
이것이 의미하는 것
- AdamStep는 Pout/Mout/Vout를 만들지만 실제 실행 슬롯에서는:
- slots[Pout] = slots[P]
- slots[Mout] = slots[M]
- slots[Vout] = slots[V]
로 바뀌고, 커널이 out에 써도 결국 in이 업데이트된다.
장점
- 레이어 구현을 SSA로 단순화하면서도 실행은 고성능 in-place 가능.
7.2 지금 alias 설계의 위험한 점
- kind 문자열 의존
- op.kind가 변경되면 alias 정책이 무력화.
- 검증 조건 부족
- adam_step에서 Pout != G만 체크.
- 실제 커널이 “out이 grad와 alias되면 안 됨”을 더 강하게 요구할 수도 있음(Mout/Vout 포함).
- 디버깅 난이도
- alias를 걸어버리면 “Pout이 별도 텐서”라는 SSA 의미가 사라짐.
개선안(바로 적용 가능)
- constraints에 alias policy를 명시:
- constraints={"inplace_ok": True, "alias_policy": "out0_in0"}
- adam_step은 "alias_policy": "adam_triplet"
- plan 단계에서 더 강한 검증:
- out_vid가 “읽기 전용으로 필요”한 다른 op에 사용되는지 분석(간단한 use-def로도 가능)
- grad input과 alias 금지를 outputs 전체에 대해 적용
- debug 모드:
- inplace_ok 무시하고 alias 비활성화 옵션 제공(정확성/디버깅 우선)
8. CudaExecutor: compile_cached / capture_prebuilt / run_compiled
8.1 compile_cached
- key = id(m.b)
- 동일 builder면 compile 결과 재사용
개선안
- optimize_ir가 Builder를 새로 만들게 되면 compile_cache key 정책이 흔들릴 수 있음.
- plan_id / hash 기반으로 바꾸는 게 더 장기적으로 안정적임.
8.2 capture_prebuilt와 모드 정책
_get_roles(mode):
- train:
- static_roles=("input","param","state")
- copy_roles=("input",)
- inference:
- static_roles=("input","param")
- copy_roles=("input",)
의미
- 캡처 시 externals는 “고정 포인터 버퍼”가 필요하므로 static_roles에 포함되어야 함.
- replay 시 매 step 바뀔 수 있는 것만 copy_roles에 포함.
train에서 param/state가 copy_roles에 없다는 게 핵심:
- 내부 고정 버퍼가 업데이트되며 그대로 유지
- 외부에서 param/state feed를 줘도 replay마다 복사하지 않음 → 사실상 무시됨
개선안(현실적으로 매우 유용)
- “reset/reload” 모드 추가:
- mode="train_reset": copy_roles=("input","param","state")
- 혹은 API 제공:
- executor.load_externals(gprog, feed, roles=("param","state"))로 ext_bufs overwrite
- 이거 없으면 “체크포인트 로딩/재시작”에서 사용자 경험이 크게 망가진다.
8.3 cache_key 설계
_cache_key는 copy_roles에 해당하는 이름들의 shape/dtype/device만 signature로 사용한다.
train에서 copy_roles=("input",)이면:
- param/state의 shape/dtype이 달라도 key가 같을 수 있음.
- 하지만 어차피 replay에서 param/state를 복사하지 않으니 “외부 주입을 허용하지 않는 정책”이면 괜찮다.
개선안
- 정책을 명시적으로 문서화해야 함:
- “train 캡처 이후 param/state는 내부에서만 유지되며, 외부에서 주입해도 반영되지 않는다.”
- 또는 strict_static_signature=True 옵션으로 static_roles도 signature에 넣을 수 있게 해두면 디버깅/안정성에 좋다.
9. graph_capture: 고정 포인터 버퍼 기반 캡처/리플레이
9.1 capture_cuda_graph 단계별 동작
(1) externals must be static
- external_vids 전부가 static_roles에 포함되어야 함
- 누락 시 에러
이건 CUDA Graph의 가장 중요한 제약(캡처 포인터 고정)을 코드로 강제한 것.
(2) externals에 대해 내부 버퍼 할당
- feed 텐서를 그대로 쓰지 않고
- alloc_from_spec으로 새 GPU 텐서를 할당한 뒤 copy
즉 캡처되는 포인터는 “사용자 feed”가 아니라 “내부 고정 버퍼(ext_bufs)”다.
(3) tmp 포함 전체 slots 채우기
- externals 외 Value들도 spec 기반으로 모두 alloc
(4) alias 적용
- plan.alias를 slot 레벨에서 반영
- 중요한 점: out_vid가 external이면 ext_bufs도 같이 갱신
(5) warmup
- 동일 버퍼에서 미리 실행해 안정화
(6) capture
- copy_roles(예: input)만 ext_buf로 copy
- with torch.cuda.graph(graph): _run_ops(...)
9.2 replay_cuda_graph
- copy_roles(보통 input)만 ext_buf로 copy
- graph.replay()
- outputs는 gprog.slots[vid]에서 반환
이것이 의미하는 것
- replay는 “입력 갱신 + 커널 실행”만 수행
- 메모리 주소/할당은 없다 (그래서 빠름)
- param/state는 내부 버퍼이므로 업데이트가 누적된다
10. 지금 구조가 “정확하게 작동하는지”를 검증하는 체크리스트
10.1 alias 실제 적용 여부
아래 조건이 True면 alias가 제대로 적용된 것:
- 캡처 후
- data_ptr(gprog.slots[Pout]) == data_ptr(gprog.slots[P])
- data_ptr(gprog.slots[Mout]) == data_ptr(gprog.slots[M])
- data_ptr(gprog.slots[Vout]) == data_ptr(gprog.slots[V])
이걸 한 번 찍으면 “weight 업데이트가 왜 slot에서 보이는지”가 완전 확정된다.
10.2 train 모드에서 param/state가 copy되지 않는지
- replay 직전에 외부 feed의 param 텐서를 바꿔도,
- gprog.slots[param_vid]가 외부 feed와 동기화되지 않아야 정상(현재 정책상)
10.3 bc1/bc2 ABI fixup 동작 여부
- bc1/bc2가 (1,)로 들어갔을 때 _apply_abi_fixups가 view(())로 바꾸는지
- 커널이 rank0 scalar로 정상 인식하는지
11. 주요 실패 모드(현실에서 터지는 것들)와 개선안
아래는 “곧바로 터질 가능성 높은 것” 위주.
11.1 Autodiff의 근본적 한계 (지금은 MLP만 되니까 안 보임)
- multi-output 미지원(outs[0]만)
- gradient accumulation 없음(덮어씀)
- elementwise/add/sub의 올바른 미분 규칙 부족
- reduce_sum/softmax/ce 같은 op의 backward 일반화 부족
개선안(우선순위 1)
- grad_map 누적(Add) 도입
- entry outputs 전체 처리
- op별 backward rule 테이블 분리(현재처럼 “일부 kind만 대충”은 확장 불가)
11.2 alias 정책의 위험
- kind 문자열에 의존
- 검증 부족
- 디버깅 어렵
개선안(우선순위 1~2)
- constraints에 alias policy 명시 + plan에서 policy 기반 처리
- debug 모드 alias off
- alias 안전성 검사(use-def 기반 최소 검사)
11.3 체크포인트/재시작 문제
train에서 param/state copy 안 함 → 외부 주입이 안 먹힘
개선안(우선순위 1)
- mode="train_reset" 또는 executor.reset_externals(...)
- 최소한 “param/state를 ext_buf로 덮어쓰기” API 제공
11.4 dtype/device 정확성
- Model.param/state는 torch.float32로 고정 생성(현재 model.py)
- TensorSpec dtype이 f16/bf16이면 mismatch 가능
개선안(우선순위 2)
- TensorSpec dtype → torch dtype 매핑해서 그대로 생성
- fp16/bf16 mixed precision을 하려면:
- param dtype과 accum dtype 분리(예: master fp32 + model fp16) 정책 필요
11.5 optimize_ir/compile_cache의 미래 충돌
optimize_ir가 Builder를 새로 만들면:
- vid가 재번호될 수 있고
- compile_cached key가 id(m.b)라 캐시가 꼬일 수 있음
개선안(우선순위 2~3)
- Builder에 graph_hash(values+ops 기반) 부여
- compiled_cache key를 graph_hash 기반으로
- 패스가 새 Builder를 만들면 remap도 함께 생성
11.6 CUDA Graph 캡처 안정성(멀티 스트림/비동기)
현재는:
- stream 인자 지원 안 함
- current_stream_u64로 op_call
개선안(우선순위 3)
- stream 정책 명확화:
- capture는 특정 stream에서만, replay도 동일 stream에서만
- 또는 per-mode per-stream graph cache 분리
- 멀티스트림/통신(op) 들어가면 반드시 동기/이벤트 정책이 필요
12. 권장 로드맵: “지금 코드 기반으로” 가장 싸게 큰 안정성을 얻는 순서
Phase 1 (바로 체감되는 안정화)
- reset/reload API 추가 (param/state 덮어쓰기)
- debug alias off 옵션
- grad accumulation + multi-output 처리
- dtype 생성 정확화(spec 기반 tensor 생성)
Phase 2 (컴파일러다운 확장)
- optimize_ir에 fuse 패스(특히 gemm+bias_add → gemm_epilogue)
- lowering/variant selection 구조 도입(여기서 registry가 의미를 갖게 됨)
- alias 안전성 검사 강화 + policy 메타데이터화
Phase 3 (실전 운영)
- checkpoint/restore 정식화(외부 저장 ↔ ext_bufs 로딩)
- multi-stream / event / overlap 실행 정책
- 프로파일/검증 모드(정확성 vs 성능) 스위치
13. 한 장짜리 “현재 AICF v2 학습 CUDA Graph 실행 요약” (정리)
- 빌드
- Layer.emit이 Builder(values/ops)에 SSA IR을 append
- Model.add가 tape 기록 + param/state 실텐서 자동 생성
- Backward
- tape 역순으로 emit_backward 호출
- grad_initial input을 seed로 주입
- parameter_grads 생성
- Optimizer
- AdamStep가 (Pout,Mout,Vout) 새 Value 생성 + inplace_ok constraints로 op 추가
- 컴파일
- optimize_ir(현재 identity)
- make_exec_plan_cuda가 alias 생성:
- adam_step: Pout→P, Mout→M, Vout→V
- 캡처
- externals(input/param/state) 전부 내부 고정 버퍼(ext_bufs)로 재할당
- alias 적용 후 warmup + torch.cuda.graph 캡처
- 리플레이
- input만 ext_buf로 copy
- graph.replay
- param/state는 내부 버퍼에서 누적 업데이트 → 학습이 진행됨
'AI Compiler framework' 카테고리의 다른 글
| 미분 그래프 구성 방식에 대한 변경 - layer 순회에서 최적화된 emit 표현의 역순회 (0) | 2026.02.21 |
|---|---|
| AICF Emit IR Model - Emit as Minimal Lowering Unit + Information Lattice Based Compilation (0) | 2026.02.21 |
| AICF v2 기술 설계 및 구현 명세서 (0) | 2026.02.17 |
| AICF v2 통합 역전파 설계 (Integrated Autodiff Design) (0) | 2026.02.17 |
| AICF 통합 학습 그래프 캡처 메커니즘 : AICF 의 Trainstep 완료 (0) | 2026.02.17 |