모든 이야기

단계를 늘렸는데 느려졌다: 파이프라인 처리량을 다시 계산하기

연산을 네 단계에서 여덟 단계로 나눴고 클록도 더 빨라졌다. 그런데 요청 두 개를 처리하는 시간은 오히려 늘었다. 파이프라인을 깊게 만들면 빨라진다는 기대와 무엇이 어긋난 것일까. 파이프라인 처리량은 단계를 통과하는 시간보다, 새로운 작업을 얼마나 자주 시작하고 결과를 얼마나 자주 내보낼 수 있는지로 결정된다.

여기서 latency는 한 작업을 수락한 뒤 결과가 준비되기까지의 시간이다. initiation interval, 줄여서 II는 연속 작업을 시작하는 데 필요한 최소 간격을 클록 사이클로 센 값이다. latency가 네 사이클이라도 II=1이면 매 사이클 새 작업을 받을 수 있다. II=4라면 네 사이클마다 한 작업만 시작할 수 있다. 두 숫자는 서로 다른 질문에 답한다.

첫 결과와 마지막 결과를 따로 계산하기

독립적인 작업이 N개 있고 출력은 항상 수락된다고 하자. 각 작업의 latency가 L사이클이며 시작 간격이 II사이클로 일정하다면, 첫 작업 수락에서 마지막 결과까지는 다음과 같다.

전체 사이클 = L + (N−1)×II

처음 수락한 작업이 L사이클 뒤에 끝나고, 마지막 작업은 그보다 (N−1)×II사이클 늦게 시작하기 때문이다. 충분히 긴 흐름의 정상 상태 처리량은 클록 주파수 f를 사용해 f / II로 볼 수 있다. 입력 부족과 stall이 없는 단순한 모델이다.

가상의 두 구성을 비교해 보자. 실제 제품의 측정값이 아닌 계산용 숫자다.

구성클록latencyII
A200MHz4사이클1사이클
B300MHz8사이클1사이클

A의 첫 결과는 4/200MHz=20ns, B는 8/300MHz≈26.7ns다. B의 클록은 빨라도 한 작업의 응답은 더 늦다. 작업 두 개라면 A는 5사이클, 25ns이고 B는 9사이클, 30ns다. 짧은 묶음에서는 시작 비용이 두드러진다.

작업 100개라면 A는 103사이클, 515ns이고 B는 107사이클, 약 356.7ns다. 긴 흐름에서는 II가 같고 f가 높은 B의 정상 상태 처리량이 유리해진다. A는 초당 2억 작업, B는 초당 3억 작업이라는 이상적인 지속 처리량을 갖는다. 첫 결과 시간과 긴 묶음의 완료 시간이 반대로 움직일 수 있다는 뜻이다.

단계를 쪼개도 II가 줄지 않는 경우

한 연산을 여러 단계로 나누었다고 해서 모든 단계가 동시에 서로 다른 작업을 처리할 수 있는 것은 아니다. 내부 자원을 작업 하나가 끝날 때까지 독점하는 구조라면 II가 latency에 가깝게 남을 수 있다. 여덟 단계라는 표현만 보고 II=1을 가정하면 안 된다.

독립 작업은 II=1로 받을 수 있어도, 작업 사이에 의존성이 있으면 상황이 달라진다. 예를 들어 acc_next = acc + term을 반복하는 가상의 누적 연산에서 다음 작업은 이전 acc 결과를 필요로 한다. 결과가 네 사이클 뒤에 나오는 구조이고 별도의 우회·재구성도 없다면, 같은 누적값에 대한 작업을 매 사이클 시작할 수 없다. 빈 단계가 많아도 필요한 입력이 아직 없는 것이다.

서로 독립적인 누적 스트림 여러 개를 번갈아 처리하면 빈 간격을 채울 수 있다. 하지만 한 스트림의 응답이 빨라지는 것은 아니다. 순서를 바꾸어도 의미가 보존되는지, 각 스트림의 상태를 어디에 저장할지까지 정해야 한다. 처리량 개선이 의존성을 자동으로 제거해 주지는 않는다.

CIRCT의 pipeline 설계 문서는 단계 사이의 값 의존성과 레지스터 경계를 구분해 표현한다. 스케줄을 바꾸거나 단계를 추가할 때도 값의 생산과 사용 관계가 보존되어야 한다는 관점은, 깊이만 보고 성능을 판단하지 않게 해 준다.

출력이 막히면 계산된 처리량은 어디로 가는가

출력이 두 사이클에 한 번만 수락된다면 II=1의 연산기라도 실제 완료 전송률은 그 수락 속도를 넘을 수 없다. 내부 버퍼가 짧은 정지를 흡수할 수는 있지만, 유한한 버퍼가 지속적인 속도 차이를 없애지는 못한다. 결국 입력에 backpressure를 전달하거나 데이터를 버리는 별도 정책이 필요하다.

파이프라인 전체가 stall되는 구조에서는 한 소비자의 정지가 모든 단계에 영향을 준다. 탄력적인 구조는 각 단계의 빈 공간을 이용해 일부 작업을 더 받아 둘 수 있다. PULP의 stream register처럼 현재 점유 상태와 하류 수락을 함께 사용하는 레지스터가 기본 구성 요소가 될 수 있다. 다만 버퍼링은 일시적인 속도 차이를 흡수하는 장치이며 평균 수락 한계를 바꾸는 장치는 아니다.

멈출 수 없는 연산 단계가 있다면 더 주의해야 한다. 입력을 중단했더라도 이미 들어간 작업은 계속 완료된다. 하류가 멈춘 순간부터 추가로 도착할 결과를 저장할 여유가 필요하다. CIRCT 문서의 non-stallable 단계와 뒤따르는 runoff 단계 설명도 이런 조건을 다룬다. 파이프라인의 제어 방식은 처리량 식의 전제가 실제로 성립하는지 결정한다.

주파수의 증가는 공짜가 아니다

단계를 늘리면 한 단계의 조합 계산을 줄일 수 있지만, 레지스터와 클록 분배, 제어 및 메타데이터 정렬의 비용이 생긴다. 각 작업의 valid와 식별자도 데이터와 같은 시간 관계로 이동해야 한다. 데이터 연산만 여덟 단계로 늘리고 완료 식별자는 네 단계 뒤에 내보내면, 더 빠른 연산기가 잘못된 작업에 결과를 붙인다.

또 단계가 작아지면 레지스터 자체의 지연과 배선이 차지하는 비중이 커질 수 있다. 한 단계 더 쪼갰다고 클록이 같은 비율로 오르는 것은 아니다. 위 비교의 200MHz와 300MHz는 설명을 위한 가정이며, 설계에서 얻을 수 있는 값은 실제 구현으로 확인해야 한다.

처음의 두 요청이 느려진 이유는 B의 시작 latency가 처리량 이익보다 크게 작용했기 때문이다. 더 깊어진 구조가 실패한 것이 아니라, 무엇을 빠르게 하려는지에 대한 질문이 달랐다. 파이프라인 처리량을 평가할 때에는 L, II, 클록, 작업의 독립성, 입력과 출력의 실제 수락 간격을 함께 보아야 한다. 그 조건을 맞추면 빠른 첫 응답과 많은 작업의 지속 처리 중 어느 목표에 단계 추가가 도움이 되는지 계산으로 판단할 수 있다.