# ADR-0040: PE_TCM Component Model — 듀얼 채널 BW 직렬화

## Status

Accepted (2026-05-20).

ADR-0014 (PE Pipeline Execution Model) 가 "PE_TCM은 BW-기반 직렬화 scratchpad
memory" 라고 언급하나 (D1), TCM 컴포넌트 자체의 정확한 동작 모델을 별도로
명시한다.

## First action (제일 처음에 하는 일)

`start()`가 호출되면 즉시 두 개의 `simpy.Resource(env, capacity=1)`을 만들고
`self._read_res` / `self._write_res`에 보관한다. 이 두 자원이 **읽기 채널**과
**쓰기 채널**을 각각 1-in-flight로 직렬화하는 단일 결정 포인트다.

런타임 첫 동작: `_worker`가 `_inbox`에서 메시지를 한 건 꺼내 타입 분기:

- `TcmRequest` (`pe_fetch_store`에서 옴): `env.process(self._handle_tcm_request)`로
  포크. 즉 **TCM의 첫 일은 "방향 (read/write)에 맞는 채널 락을 잡는 것"**.
  락 획득 후 `bw > 0 and nbytes > 0` 이면 `delay_ns = nbytes / bw` 만큼
  `env.timeout`, 그리고 `req.done.succeed()`.
- 그 외 (Transaction): `env.process(self._forward_txn)`로 포크 (legacy fabric
  통과 경로).

생성 시점에 `node.attrs["read_bw_gbs"]` / `node.attrs["write_bw_gbs"]`
(default 각 `512.0 GB/s`) 를 읽어 보관해 둔다.

## Context

PE 파이프라인 (ADR-0014 D1, D6) 에서 PE_TCM은 다음 두 종류의 트래픽을 받는다:

1. **PE_FETCH_STORE → PE_TCM의 `TcmRequest`** — TCM ↔ Register File 전송 시,
   PE_FETCH_STORE가 TCM의 BW로 직렬화된 access latency를 받아오기 위해 짧은
   sideband 요청을 보낸다 (`direction = "read"` 또는 `"write"`, `nbytes`,
   `done` 이벤트).
2. **legacy Transaction forwarding** — 토폴로지 그래프 상에서 TCM이 통과 노드로
   잡힐 가능성에 대비한 일반 forwarding 경로 (현재 critical path에서는 사용되지
   않으나 보존됨).

문제: ADR-0014는 "PE_TCM은 BW-기반 직렬화"라고만 언급한다. 그러나 코드에는
명시적으로:

- **읽기와 쓰기는 별도 채널이며 동시 진행 가능**, 다만 같은 방향끼리는
  cap=1로 직렬화된다.
- BW는 `read_bw_gbs` / `write_bw_gbs` 두 값으로 분리 설정 가능하다.
- `delay_ns = nbytes / bw_gbs` 공식 (단위 환산: GB/s × ns ≈ B 라는 약식).
- nbytes==0이면 BW 항을 건너뛰지만 채널 락은 잡는다.
- `run()`은 `overhead_ns` (default 0.0) 만큼 yield 하나, 이는 legacy fabric
  경로(Transaction forwarding)에서만 사용된다.

이 모든 사항을 별도 ADR로 못 박을 필요가 있다. 특히 "왜 read/write가 분리
채널인가" 와 "BW는 누가 결정하는가" 는 향후 누군가가 capacity=2 등으로 변경하려
할 때 명확한 근거가 필요한 항목이다.

## Decision

### D1. 듀얼 채널 — read와 write는 독립 자원

`_read_res = simpy.Resource(env, capacity=1)`,
`_write_res = simpy.Resource(env, capacity=1)`.
같은 방향의 동시 요청은 자원 큐에서 직렬화되나, 다른 방향끼리는 동시에 진행 가능.
이는 실제 HW에서 TCM이 듀얼 포트 (read port + write port) 로 운용되는 모델과
정합되며, GEMM 파이프라인에서 fetch(read)와 store(write)가 시간상 겹치는 정상
케이스를 BW-직렬화 모델로 표현하기 위해 의도된 분리다.

### D2. 단일 채널의 BW 모델 — `nbytes / bw_gbs`

채널 락 획득 후, `nbytes > 0 and bw > 0`이면 `yield env.timeout(nbytes / bw_gbs)`.
단위 약식은 GB/s × ns ≈ B 로, 시뮬레이터 전체에서 사용하는 BW 공식과 동일
(ADR-0033 참고 — 시뮬레이터는 일관된 약식 단위를 사용한다).

- `nbytes == 0`: BW 항은 0이지만 락은 잡혔다가 즉시 풀린다. 이 케이스가 의도된
  이유: 빈 fetch/store를 보내는 plan generator가 PE_FETCH_STORE 측에서 `nbytes`만
  0으로 채워 보내는 경우에도, TCM 측의 op_log / 채널 회계가 일관되게 한 번
  소비된다.
- `bw == 0` (config 실수): timeout 호출 자체를 skip하므로 0-time pass. 정상
  세팅에서는 발생하지 않는다.

### D3. BW는 `node.attrs`의 `read_bw_gbs` / `write_bw_gbs`로 설정

기본값 `512.0 GB/s`. 토폴로지 빌더 (`topology/builder.py`) 가 `pe_template`에서
TCM을 인스턴스화할 때 해당 attrs를 전달한다. 기본값 변경은 ADR-0014 D1 또는
ADR-0033 latency model 측의 의사결정과 함께 가야 한다.

### D4. TcmRequest의 schema는 PE_TCM이 owner다

`@dataclass TcmRequest(direction: str, nbytes: int, done: simpy.Event, tag: str = "")`
는 `components/builtin/pe_tcm.py`에 정의된다. PE_FETCH_STORE는 이 dataclass를
import해서 생성·송신만 한다. 호출자 측이 schema를 정의하지 않는 이유:

- BW 직렬화의 의미는 TCM 측 책임 — 어떤 필드가 직렬화 결정에 쓰이는가는 TCM이
  결정한다.
- `direction` 문자열을 `"read"` / `"write"` 둘로 좁히는 유효값 검증도 TCM 측에
서 담당 (`_handle_tcm_request`의 if/else 분기).

### D5. legacy Transaction forwarding 경로의 보존

`_worker`가 `TcmRequest`가 아닌 메시지를 받으면 `_forward_txn`으로 보낸다. 이때
`run()`의 `overhead_ns`가 적용된다. 현재 표준 PE 파이프라인에서는 TCM이
Transaction의 통과 노드로 잡히지 않으나, fabric 토폴로지가 향후 변경될 때를
위해 보존한다 (D1 의 사용 패턴과 직교).

이 경로는 op_log 측에서 일반 Transaction 회계로 잡히며, BW 채널 락은 잡지 않는다.

### D6. PE_TCM은 자체 데이터 저장소가 아니다 (timing only)

TCM은 **시간만** 모델링한다. 실제 데이터 페이로드는 sim_engine의 별도
`memory_store` (있다면) 가 보관하고, TCM 컴포넌트는 그것을 갱신하지 않는다.
PE_FETCH_STORE도 TcmRequest를 통해 BW 지연만 받아오고 실제 register 컨텐츠는
별도 경로로 다룬다 (ADR-0020 2-pass data execution 모델 — Phase 2에서 데이터
처리).

## Alternatives Considered

### A1. 단일 채널 (capacity=2 의 read+write 공유)

기각. fetch(read)와 store(write)가 시간상 겹치는 정상 케이스를 인공적으로
직렬화하게 되어 PE 파이프라인의 BW upper bound가 잘못 모델링된다.

### A2. 채널 capacity > 1 (예: 2-banked TCM)

기각. 현재 HW 모델은 단일 bank 가정. 멀티-bank로 확장하고 싶다면 별도 ADR이
필요하며, 그때 D1을 supersede한다. 지금 단계에서 capacity를 늘리면 BW upper
bound는 그대로인데 명목상의 직렬화만 헐거워져 실제 모델 정확도 ↓.

### A3. BW 공식을 `nbytes / bw + overhead_ns`로 일반화

기각. `overhead_ns`는 D5의 legacy forwarding 경로에만 사용한다. fetch/store
critical path에 추가 overhead가 필요해지면, 그것은 TCM이 아니라 PE_FETCH_STORE
측 `run()` 또는 register-file access 모델에 두는 것이 책임 경계 측면에서 더
적절하다.

## Consequences

- TCM의 BW 회계가 ADR-level에서 굳어지므로, GEMM/Math sweep의 op_log 해석 시
  "왜 fetch와 store가 동시에 진행되었나" / "왜 같은 방향만 직렬화되나" 같은
  질문이 빠르게 D1으로 해결된다.
- 미래의 멀티-bank TCM이나 read/write 비대칭 BW 모델 변경 시 영향 범위가
  명확해진다 (D1·D2·D3 중 어디를 수정하는지).
- TCM이 데이터 저장소가 아니라는 점(D6)이 명시되어, ADR-0020 2-pass execution
  과의 책임 경계가 견고해진다.
