Files
kernbench2/docs/onboarding/di-presentation.md
T
ywkang 687c98086d ADR housekeeping: category prefixes, lifecycle folders, retroactive 0034-0037
Filename + lifecycle:
- ADR rename to ADR-NNNN-<cat>-title.md with 8 3-letter category prefixes
  (dev / mem / lat / prog / algo / par / api / ver). Numbers stay immutable.
- ADR Lifecycle split into 3 folders, documented in CLAUDE.md Part 2:
  docs/adr/ (Accepted), docs/adr-proposed/ (Proposed/Stub/Draft),
  docs/adr-history/ (Superseded/Merged). Status field gains "Draft" for
  retroactive docs pending verification.

Merges (one ADR per topic, no change-history annotations):
- ADR-0017 absorbs ADR-0019 (Cube NOC + per-PE HBM connectivity, 10 D-items)
- ADR-0014 absorbs ADR-0021 (PE pipeline execution model, 8 D-items incl.
  TileToken self-routing and multi-op composite epilogue scope)
- ADR-0023 absorbs docs/ipcq-dma-codesign-hw.md as new "HW Realization
  Notes (Informative)" section (D16-D23 + Open HW Questions). codesign-hw.md
  deleted; ADR-0019/0021 moved to adr-history with one-line stub status

Retroactive documentation (G4 closures, code-verified):
- ADR-0037 forwarding component (TransitComponent: first-flit overhead,
  serial worker, path-based routing, single impl/multiple names)
- ADR-0036 IO_CPU component (target_start_ns global barrier stamping,
  per-cube fan-out, response aggregation)
- ADR-0035 M_CPU & M_CPU.DMA component (3 fan-out paths, DMA Resources,
  target_start_ns passthrough)
- ADR-0034 HBM controller internal design (per-PC state, address-based
  selection, flit-aware per-flit commit, async finalize, command-only
  fallback path)

Content updates:
- ADR-0010 expanded to full CLI surface (run/probe/web), retitled
  "Command Line Interface and Execution Semantics"
- ADR-0007 D2 rewritten to current state; ADR-0015 supersession notes pruned
- ADR-0005 wrapped in Decision header with D1-D5; ADR-0022 metadata
  block replaced with standard Status header
- ADR-0024 trimmed to rank=SIP launcher essentials (D1-D4);
  ADR-0027 cleaned of supersession history
- ADR-0033 D6 cleanup: address-based PC selection moved out of future-work
  (now documented in ADR-0034 D3); related D1/D3 wording realigned
- Cross-references back-filled in 5 ADRs (G3 gaps closed)

Onboarding docs split:
- docs/onboarding/ created
- moved: hw-architecture-overview.md, latency-model.md, di-presentation.md,
  ccl-author-guide{,.en}.md
- references updated in README, ADR-0023{,.en}, src/kernbench/ccl/__init__.py

Source / test / yaml: ADR-NNNN cross-references in docstrings and YAML
comments updated after the merges (ADR-0021->0014 D6, ADR-0019->0017 D8).
No behavior change.

Tooling:
- tools/verify_adr_lang_pairs.py + tests/test_verify_adr_lang_pairs.py
  (ADR EN/KO pair invariant checker)
- .claude/commands/report.md tracked (/report slash command)
- .gitignore: allow .claude/commands/*.md while keeping settings files ignored

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 01:15:55 -07:00

11 KiB

실무 DI 패턴: kernbench 구현으로 배우는 Dependency Injection


슬라이드 1 — 오늘 이야기할 것

질문: 코드를 어떻게 설계해야 테스트하기 쉽고, 갈아끼우기 쉬울까?

답: Dependency Injection (DI)

오늘은 이론이 아니라 실제로 돌아가는 시뮬레이터 코드를 보면서 배웁니다.

kernbench
└── AI 가속기 하드웨어를 Python으로 시뮬레이션하는 프레임워크
    - 수십 개의 하드웨어 컴포넌트 (NOC, HBM, PE, CPU...)
    - 각 컴포넌트는 런타임에 교체 가능
    - 테스트에서 Mock 컴포넌트로 즉시 대체 가능

슬라이드 2 — DI가 없으면 어떤 일이 생기나

# ❌ DI 없는 코드
class IoCpuComponent:
    def run(self, env, nbytes):
        router = PathRouter()        # 직접 생성 — 교체 불가
        hbm = HbmCtrlComponent()    # 직접 생성 — 교체 불가
        yield env.timeout(10.0)

문제:

  • 테스트할 때 실제 PathRouterHbmCtrl이 항상 따라온다
  • 컴포넌트를 Mock으로 바꾸려면 소스 코드를 수정해야 한다
  • 다른 topology(다른 라우팅 전략)를 쓰고 싶으면 또 수정

클래스가 자기 의존성을 스스로 만들면, 그 클래스는 의존성과 결합된다


슬라이드 3 — DI의 핵심 원칙

의존성은 밖에서 만들어서 안으로 넣어준다

┌────────────────────────────┐
│  조립자 (Assembler)         │  ← 누가 무엇을 쓸지 결정
│  GraphEngine.__init__      │
└────────────┬───────────────┘
             │ ctx 주입
             ▼
┌────────────────────────────┐
│  컴포넌트 (Component)       │  ← 어떻게 동작하는지만 알면 됨
│  IoCpuComponent            │
│    self.ctx.router.find_path(...)  ← 그냥 사용
└────────────────────────────┘

세 가지 역할 분리:

  1. Interface — 무엇을 할 수 있는가 (ComponentBase)
  2. Implementation — 어떻게 하는가 (IoCpuComponent, HbmCtrlComponent, ...)
  3. Assembler — 무엇을 연결할 것인가 (GraphEngine)

슬라이드 4 — 패턴 1: Constructor Injection

생성자로 의존성을 받는다

# kernbench/components/base.py

class ComponentBase(ABC):
    def __init__(self, node: Node, ctx: ComponentContext | None = None):
        self.node = node
        self.ctx = ctx          # 외부에서 주입받은 의존성
        self.in_ports: dict[str, simpy.Store] = {}
        self.out_ports: dict[str, simpy.Store] = {}
# 사용 측 — ctx를 직접 만들지 않는다
class IoCpuComponent(ComponentBase):
    def _dispatch(self, env, txn):
        path = self.ctx.router.find_node_path(...)   # ctx는 이미 들어와 있음
        yield self.out_ports[next_hop].put(...)

언제 쓰나:

  • 컴포넌트가 살아있는 동안 의존성이 바뀌지 않을 때
  • 의존성 없이는 컴포넌트가 동작하지 않을 때 (필수 의존성)

슬라이드 5 — Context Object 패턴

의존성이 많아지면 묶어서 하나로

# kernbench/components/context.py

@dataclass
class ComponentContext:
    router: PathRouter              # 라우팅 정책
    resolver: AddressResolver       # 주소 해석
    positions: dict[str, ...]       # 물리적 위치 정보
    ns_per_mm: float                # 전파 지연 상수
    edge_map: dict[...]             # 엣지 정보
    spec: dict                      # 토폴로지 스펙

왜 Context로 묶나?

  • 생성자 인자가 6개면 → 컴포넌트 추가할 때마다 시그니처 변경
  • Context 하나면 → 새 필드 추가해도 기존 컴포넌트 무영향
  • 컴포넌트는 필요한 것만 꺼내 쓴다
class TwoDMeshNocComponent(ComponentBase):
    def _route(self, env, txn):
        src_pos = self.ctx.positions.get(prev_hop)   # 위치만 사용
        ns_per_mm = self.ctx.ns_per_mm               # 상수만 사용
        # router, resolver 등은 건드리지 않음

슬라이드 6 — 패턴 2: Registry + Factory

문자열 키 → 클래스 매핑으로 런타임 교체

# kernbench/components/base.py

class ComponentRegistry:
    _registry: dict[str, type[ComponentBase]] = {}

    @classmethod
    def register(cls, impl: str, component_cls: type[ComponentBase]):
        cls._registry[impl] = component_cls

    @classmethod
    def create(cls, node, overrides=None, ctx=None) -> ComponentBase:
        if overrides and node.impl in overrides:
            return overrides[node.impl](node, ctx)   # 1순위: 호출자 override
        if node.impl in cls._registry:
            return cls._registry[node.impl](node, ctx)  # 2순위: 등록된 구현
        return DefaultComponent(node, ctx)           # 3순위: 기본값 fallback

Resolution 우선순위:

overrides[impl]        ← 테스트/실험용 주입
  ↓ (없으면)
_registry[impl]        ← 프로덕션 구현
  ↓ (없으면)
DefaultComponent       ← 안전한 fallback

슬라이드 7 — Registry 등록 방식

# kernbench/components/builtin/__init__.py

from kernbench.components.base import ComponentRegistry
from kernbench.components.builtin.noc import TwoDMeshNocComponent
from kernbench.components.builtin.io_cpu import IoCpuComponent
# ...

ComponentRegistry.register("noc_2d_mesh_v1", TwoDMeshNocComponent)
ComponentRegistry.register("io_cpu_v1",       IoCpuComponent)
ComponentRegistry.register("hbm_ctrl_v1",     HbmCtrlComponent)
# ...

topology.yaml (설정 파일)

nodes:
  - id: sip0.cube0.noc
    impl: noc_2d_mesh_v1    # ← 이 문자열이 Registry 키

흐름:

YAML → impl 문자열 → Registry.create() → 실제 컴포넌트 인스턴스

impl 문자열만 바꾸면 동작이 바뀐다. 코드 수정 없음.


슬라이드 8 — 패턴 3: Override Injection (테스트용)

호출자가 특정 impl만 갈아끼운다

# tests/test_component_registry.py

class SpyXbar(ComponentBase):
    calls = 0

    def run(self, env, nbytes):
        SpyXbar.calls += 1
        yield env.timeout(0)


# 테스트에서 xbar_v1만 SpyXbar로 교체
engine = GraphEngine(
    graph,
    component_overrides={"xbar_v1": SpyXbar}   # ← 이것만 추가
)

result = engine.run(msg)
assert SpyXbar.calls > 0    # Xbar가 실제로 호출됐는지 검증

핵심: 테스트 코드가 프로덕션 코드를 수정하지 않는다


슬라이드 9 — 조립자: GraphEngine

컴포넌트를 생성하고 연결하는 유일한 곳

# kernbench/sim_engine/engine.py

class GraphEngine:
    def __init__(self, graph, component_overrides=None):

        # 1. 공유 의존성 생성
        ctx = ComponentContext(
            router=PathRouter(graph),
            resolver=AddressResolver(graph),
            positions={nid: n.pos_mm for nid, n in graph.nodes.items()},
            ns_per_mm=...,
        )

        # 2. 컴포넌트 생성 (DI: ctx 주입)
        self._components = {
            node_id: ComponentRegistry.create(node, overrides, ctx)
            for node_id, node in graph.nodes.items()
        }

        # 3. 포트 연결 (배선)
        for e in graph.edges:
            store = simpy.Store(self._env)
            self._components[e.src].out_ports[e.dst] = store
            self._components[e.dst].in_ports[e.src] = store

생성 → 주입 → 연결 — 이 세 단계가 한 곳에서만 일어난다


슬라이드 10 — 전체 구조 한눈에 보기

topology.yaml
    │ impl: "noc_2d_mesh_v1"
    ▼
GraphEngine.__init__()                     ← 조립자
    │
    ├── ComponentContext 생성               ← 공유 의존성 묶음
    │     ├── PathRouter
    │     ├── AddressResolver
    │     └── positions, ns_per_mm, ...
    │
    ├── ComponentRegistry.create(node, overrides, ctx)
    │     ├── overrides["noc_2d_mesh_v1"]? → SpyNoc (테스트)
    │     ├── registry["noc_2d_mesh_v1"]?  → TwoDMeshNocComponent (프로덕션)
    │     └── fallback                     → DefaultComponent
    │
    └── 포트 배선: out_ports / in_ports 연결

Component (TwoDMeshNocComponent)
    └── self.ctx.positions, self.ctx.ns_per_mm 사용
        (라우터, 리졸버는 건드리지 않음 — 필요한 것만)

슬라이드 11 — 무엇을 얻었나

상황 DI 없이 DI 있이
NOC 알고리즘 교체 소스 코드 수정 YAML에서 impl 문자열 변경
Xbar 동작 검증 실제 HW 전부 구동 overrides={"xbar_v1": SpyXbar}
새 컴포넌트 추가 기존 코드 수정 register("new_v1", NewComp)
컨텍스트 필드 추가 모든 생성자 수정 ComponentContext에 필드 추가
테스트 격리 불가능 필요한 것만 override

슬라이드 12 — 실무 적용 체크리스트

설계할 때 물어볼 것:

  1. 이 클래스가 직접 new(생성)하는 것은 무엇인가? → 생성하는 것 = 교체할 수 없는 것. 생성자로 받을 수 없는지 검토.

  2. 의존성이 3개 이상이면? → Context Object로 묶어라.

  3. 테스트에서 이 클래스를 단독으로 실행할 수 있는가? → 없다면 DI가 필요하다는 신호.

  4. 설정(YAML/config)으로 동작을 바꾸고 싶은가? → Registry + 문자열 키 패턴.

  5. 누가 조립하는가? → 조립자는 하나여야 한다. 컴포넌트 안에 조립 로직이 있으면 안 된다.


슬라이드 13 — 안티패턴: 이것은 하지 말자

# ❌ 서비스 로케이터 (컴포넌트 안에서 registry 호출)
class BadComponent(ComponentBase):
    def run(self, env, nbytes):
        router = ComponentRegistry.get("router")  # 컴포넌트가 직접 찾는다
        ...

# ❌ 전역 싱글톤 직접 참조
class BadComponent(ComponentBase):
    def run(self, env, nbytes):
        router = GlobalRouter.instance()          # 교체 불가
        ...

# ❌ 생성자 안에서 의존성 생성
class BadComponent(ComponentBase):
    def __init__(self, node):
        self.router = PathRouter(node.graph)      # 테스트에서 격리 불가

공통 문제: 컴포넌트가 자기 의존성을 스스로 해결한다 → 결합도 증가


슬라이드 14 — 요약

DI = 의존성의 생성과 사용을 분리하는 것

생성  →  Registry / Assembler (GraphEngine)
사용  →  Component (IoCpuComponent, TwoDMeshNocComponent, ...)

kernbench에서 배운 패턴 3가지:

  1. Constructor Injection — 필수 의존성은 생성자로
  2. Context Object — 의존성 묶음을 하나의 dataclass로
  3. Registry + Override — 문자열 키로 구현체 선택, 테스트에서 교체

결과: 141개 테스트, YAML 한 줄로 컴포넌트 교체, 프로덕션 코드 수정 없이 Mock 주입


참고 코드: kernbench/src/kernbench/components/