valid를 지워도 일이 남는다: 파이프라인 flush의 책임
파이프라인의 valid 비트를 모두 0으로 만들었는데 다음 요청이 들어오지 않는다. 예약한 자원은 여전히 사용 중으로 표시되어 있고, 늦게 돌아온 완료 신호는 새 요청의 카운터를 줄인다. 파이프라인 flush는 결과를 보이지 않게 만드는 것뿐 아니라, 취소한 거래가 남긴 책임을 정리하는 동작이다. 비트를 지웠다는 사실과 일을 취소했다는 사실은 같지 않다.
flush는 잘못된 경로의 작업이나 더 이상 필요 없는 작업을 파이프라인에서 제거할 때 사용하는 제어다. 여기서는 내부에서 취소 가능한 거래만 다루고, flush가 오는 에지에는 입력 수락과 출력 전송을 막는 규칙을 선택한다. 외부 장치가 이미 수락한 쓰기를 이 신호 하나로 되돌릴 수 있다고 가정하지 않는다.
같은 에지에 무엇이 우선하는지 정하기
한 칸 버퍼에 항목 A가 있고, 상류는 B를 제시하며 하류는 ready=1이라고 하자. 동시에 flush가 왔다. A를 내보내고 B를 받는지, 둘 다 버리는지, A만 버리고 B를 받는지는 인터페이스가 정해야 한다. “flush니까 지운다”는 설명만으로는 충분하지 않다.
다음은 flush가 우선하며 같은 에지의 거래를 막는 설명용 구조다. 연결된 내부 생산자와 소비자도 flush의 취소 의미를 공유한다고 가정한다.
assign out_valid = valid_q && !flush;
assign in_ready = !flush && (!valid_q || out_ready);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
valid_q <= 1'b0;
else if (flush)
valid_q <= 1'b0;
else if (in_ready)
valid_q <= in_valid;
end
always_ff @(posedge clk) begin
if (rst_n && in_valid && in_ready)
data_q <= in_data;
end
flush=1이면 out_valid=0, in_ready=0이다. A는 그 에지에 전송되지 않고 valid가 지워진다. B는 수락되지 않았으므로 생산자가 책임을 가진다. 다음 에지에 flush가 해제되면 빈 버퍼에서 B를 받을 수 있다. flush는 샘플링 에지 전에 안정되어 있다는 동기 제어 조건이다.
이 예제는 일반적인 “제시한 valid는 수락까지 유지한다”는 규칙에 취소 예외를 추가한다. 예외를 모르는 외부 소비자에게 out_valid를 갑자기 내리면 프로토콜 위반이 될 수 있다. 따라서 실제 모듈에 이 코드를 넣기 전에 flush가 어느 경계까지 공유되는지 확인해야 한다.
보이지 않는 예약도 취소해야 한다
요청을 수락할 때 결과 저장 슬롯 하나를 예약하고, 완료할 때 예약을 풀었다고 하자. accepted와 completed만 세면 flush로 사라진 거래는 영원히 미완료로 남는다. 수량 관계에는 취소도 포함되어야 한다.
미완료 수 = 수락 수 − 정상 완료 수 − 취소 수
가령 설명용 실행에서 세 요청을 수락하고 하나를 완료한 뒤 나머지 두 개를 취소했다면 미완료 수는 3−1−2=0이다. valid를 지웠는데 예약 수가 2로 남아 있으면, 빈 파이프라인이 새 요청을 막는다. 거래마다 잡았던 credit, 목적지 busy 비트, 반환 슬롯 등 관련 상태가 같은 취소 집합을 따라야 한다.
모든 pending 작업을 취소한다면 카운터를 0으로 만드는 방식이 가능할 수 있다. 일부만 취소한다면 어떤 항목이 남는지 식별해야 한다. 복수 단계의 valid 개수를 단순히 더하는 것도 조심해야 한다. 한 거래의 데이터와 제어가 다른 곳에 중복 표현되어 있다면 같은 거래를 두 번 취소한 것으로 셀 수 있다.
계산은 끝나지 않았는데 책임은 끝났다면
취소한 작업이 멈출 수 없는 연산기에 들어가 있으면 flush 뒤에도 결과가 도착할 수 있다. 현재 파이프라인에는 새 요청 C가 있고 pending=1인데, 이전 A의 완료가 돌아와 pending을 0으로 만들면 새 요청의 책임을 잘못 지운다. 결과를 무시하는 것과 자원 회계에서 무시하는 것 모두 필요하다.
거래 식별자나 세대 표시를 함께 보내면 결과가 어느 작업에 속하는지 구분할 수 있다. flush 뒤의 새 세대와 이전 세대가 다르면 이전 완료를 출력과 카운터 갱신에서 제외하는 방식이다. 단순히 완료 신호가 왔다는 이유로 모든 pending을 감소시키면 안 된다.
세대 표시는 유한한 비트 수를 가진다. 오래된 결과가 남아 있는 동안 번호를 다시 쓰면 새 작업과 충돌할 수 있다. 따라서 사용한 비트 수뿐 아니라 최대 잔존 시간과 번호 재사용 규칙도 필요하다. 태그를 추가했다는 사실만으로 무한히 늦는 응답까지 해결했다고 주장할 수는 없다.
이미 바깥에서 수락한 동작은 다른 종류의 문제다
출력 요청을 외부 인터페이스가 수락했다면 내부 valid를 내리는 것으로 그 요청의 존재를 없앨 수 없다. 외부 장치에 쓰기가 이미 전달되었거나 응답이 나중에 돌아오는 상황이다. 취소 가능한 별도 프로토콜이 없다면 응답을 받아 정리하거나, 부작용이 확정되기 전까지 요청의 발행을 늦추는 구조가 필요하다.
BOOM의 ROB 문서는 실행 완료와 commit을 구분하고, 예외 및 flush에 따라 진행 중인 상태를 정리하는 방식을 설명한다. 그 구분을 일반적인 파이프라인에 가져오면 “계산했음”, “외부에서 관측 가능해짐”, “더 이상 취소할 수 없음”을 한 완료 비트에 섞지 않게 된다.
이 글의 한 칸 예제는 아직 외부에 전송하지 않은 항목만 취소한다. 외부에 도달한 작업의 rollback까지 구현한 코드가 아니다. 시스템에서 취소 경계를 어디에 둘지 정하는 일이 먼저다.
다른 구현의 flush 규칙은 그대로 가져와야 한다
공개 모듈을 사용할 때에도 flush와 새 valid의 동시성을 살펴봐야 한다. PULP의 flush 가능한 spill register는 flush와 입력 valid가 동시에 높은 조건을 금지하는 assertion을 둔다. 이 글의 입력 ready 차단 방식과 동일한 인터페이스 규칙은 아니다.
같은 flush라는 이름이어도 정상 거래로 배출하는지, 즉시 점유 상태를 없애는지, clear와 다르게 쓰는지 구현마다 의미가 달라질 수 있다. 어댑터는 타이밍만 맞출 것이 아니라 이런 제어 계약도 맞춰야 한다.
검증에서는 flush와 stall, 새 요청, 완료가 같은 에지에 겹치는 상황을 따져 볼 수 있다. 각 거래가 정상 완료되었는지 취소되었는지 정확히 한 번 설명되어야 한다. 아무 설명 없이 사라지거나 두 번 정리되는 거래가 없다면 수량과 소유권의 계약이 맞아 가고 있는 것이다.
처음의 빈 파이프라인은 실제로 비어 있지 않았다. payload는 없어졌지만 예약과 늦은 완료의 의미가 남아 있었다. 파이프라인 flush를 완성하려면 출력 억제, 취소한 자원의 반환, 이후 응답의 구분, 외부 부작용의 경계를 함께 정의해야 한다. 그때 valid를 지우는 작은 동작이 시스템의 일관된 취소로 연결된다.