모든 이야기

ready를 늦게 끊을 때 spill register가 받아 주는 한 항목

긴 ready 경로를 끊으려고 레지스터를 넣었더니 데이터 한 개가 사라진다. 하류가 멈췄다는 사실은 한 사이클 늦게 올라왔는데, 상류는 그동안 계속 보냈기 때문이다. spill register는 늦게 도착하는 정지 신호가 남긴 데이터 한 개를 받아 둘 여유를 제공한다. 타이밍 경계를 추가하는 일이 저장 공간의 문제로 바뀌는 사례다.

spill은 넘쳐 나온 항목을 잠시 받는다는 뜻이다. 구현마다 명칭과 배치가 다르지만, 여기서는 정상 출력을 보관하는 슬롯에 여유 슬롯 하나를 더 둔 두 항목짜리 구조를 생각한다. 단일 클록의 ready-valid이며, 생산자는 수락 전까지 같은 데이터를 유지한다. 아래 값과 추적은 구조를 설명하기 위한 가상 예제다.

정지 사실을 늦게 알리는 순간

일반적인 한 칸 버퍼는 비었거나 하류가 현재 항목을 수락하면 상류를 받을 수 있다. 이 조건을 바로 계산하면 output_ready가 input_ready까지 조합으로 전달된다. 여러 단계를 통과하는 ready 경로가 길어질 수 있다.

이 경로를 끊으면 상류는 하류의 현재 ready를 즉시 볼 수 없다. 이미 “받을 수 있다”고 알린 에지에는 그 약속을 지켜야 한다. 현재 슬롯에 A가 있는 상태에서 하류가 갑자기 ready=0이 되고, 상류가 B를 보내는 경우를 생각하자. A는 나가지 못한다. 그 에지에 상류에게 ready=1을 제시했다면 B도 받아야 한다.

저장 슬롯이 하나뿐이면 A를 B로 덮어쓰거나 B를 버리는 선택밖에 없다. 둘 다 수락한 데이터를 보존한다는 계약을 깨뜨린다. 여유 슬롯이 있으면 A와 B를 함께 유지하고 다음 에지부터 상류 ready를 낮출 수 있다. 타이밍 단절의 비용은 정확히 이 수락 여유에서 드러난다.

두 슬롯이 꽉 찼다가 다시 흐르는 추적

두 슬롯의 항목 순서는 오래된 것이 먼저라고 정하자. input_ready는 현재 저장 상태만으로 계산하며, 두 슬롯이 차 있으면 0이다. 따라서 현재 하류 ready에서 상류 ready로 직접 이어지는 경로가 없다.

에지직전 저장 항목상류 ready하류 ready에지에서 일어나는 일
0A10B를 수락해 A, B 보관
1A, B00둘 다 유지
2A, B01A를 출력, B만 남음
3B11B를 출력하며 C를 수락
4C11C를 출력하며 D를 수락

에지 2에는 하류가 다시 받을 준비가 되어도 상류는 아직 ready=0을 본다. 그 에지의 저장 상태가 꽉 차 있었기 때문이다. 그러나 출력에는 A와 B가 차례로 준비되어 있어서, 이 추적에서는 하류가 재개한 뒤 출력에 빈 사이클이 생기지 않는다. 입력이 한 번 늦게 재개되는 것과 출력의 지속 처리량은 같은 현상이 아니다.

이 구조는 ready를 무조건 한 에지 늦춰 복사하는 것과도 다르다. 상류 ready를 현재의 등록된 저장 상태에서 계산한다. 추가 저장이 정지 전달의 지연을 흡수하기 때문에, 데이터 보존과 타이밍 경계를 함께 만들 수 있다.

한 슬롯만 등록하면 처리량이 반으로 보일 수 있다

한 칸만 두고 input_ready=NOT full로 계산하면 하류 ready 경로를 끊을 수는 있다. 하지만 슬롯이 차 있는 동안은, 그 에지에 출력이 나가더라도 입력을 받지 않는다. 입력 수락 후 출력, 다시 입력 수락 후 출력이라는 반복이 되어 지속 입력에서도 두 에지에 한 항목만 처리하는 구성이 될 수 있다.

두 슬롯 구조는 한 항목이 남아 있을 때 입출력을 함께 수행할 여유를 제공한다. 항상 처리량이 두 배라는 제품 성능 주장은 아니다. 하류가 자주 멈추거나 생산자가 느리면 차이는 작아진다. 중요한 것은 연속 입력과 연속 수락이라는 조건에서 버퍼가 만드는 불필요한 빈 사이클을 줄일 수 있다는 점이다.

PULP의 spill register 구현은 두 데이터 저장소와 각각의 점유 상태를 두고, 오래된 보관 항목을 먼저 내보내는 실제 사례다. 이 글의 표는 이해를 위한 FIFO식 추적이며 원문의 내부 레지스터 이름을 그대로 대응시킨 것은 아니다.

타이밍을 끊는다고 latency가 사라지지는 않는다

등록된 출력이 있다면 입력에서 출력까지의 최소 latency가 추가될 수 있다. 반면 이미 데이터를 보유한 정상 흐름에서는 매 에지 한 항목씩 처리할 수 있다. 첫 항목이 나오는 시간과 항목 사이의 간격을 따로 봐야 한다.

넓은 payload에서는 여유 슬롯 하나의 저장 비용도 커진다. 데이터만 늘어나는 것이 아니라, 거래 식별자나 오류 플래그 같은 메타데이터도 함께 보관해야 한다. payload와 제어 필드를 다른 시점에 넘기면 같은 항목의 정보가 엇갈린다. 경로를 끊으려는 목적과 저장 폭의 비용을 구현 결과로 함께 판단해야 한다.

또 모든 spill이라 불리는 블록이 모든 조합 경로를 끊는 것은 아니다. transparent 또는 bypass 옵션을 켜면 입력과 출력이 직접 이어질 수 있다. 위 PULP 구현에도 bypass 분기가 있다. 모듈 이름으로 타이밍 경계를 추측하지 말고 실제 파라미터와 연결을 확인해야 한다.

저장된 것을 버리는 제어는 별도의 계약이다

reset이나 clear가 점유 상태를 지우면, 수락했던 항목이 출력되지 않고 사라질 수 있다. 시스템이 그런 취소를 허용한다면 양쪽이 같은 사건을 알아야 한다. 정상 ready-valid 거래에서는 수락한 항목의 보존을 약속했는데, 숨겨진 clear가 이를 깨면 버퍼는 정상 동작해도 시스템의 수량 추적이 틀어진다.

flush를 가진 구현 역시 새 valid와 flush가 동시에 오는 조건을 확인해야 한다. PULP의 해당 구현은 이 동시성을 금지하는 assertion을 둔다. 다른 프로젝트의 구조를 사용할 때에는 타이밍 효과만 가져오는 것이 아니라, 이런 제어 조건도 함께 가져와야 한다.

처음의 데이터 손실은 레지스터 한 개의 오류가 아니었다. 늦게 전달된 정지 전에 이미 수락한 데이터의 자리를 준비하지 않은 오류였다. **spill register를 이해하는 기준은 '신호를 한 번 등록한다'보다 '이미 허용한 항목을 어디에 보관하는가'**다. 그 자리를 확보하고 순서와 동시 입출력, 취소의 경계를 정의하면 ready 경로를 끊으면서도 거래의 약속을 지킬 수 있다.

ready를 늦게 끊을 때 spill register가 받아 주는 한 항목 · ariser