함께 실행되는 두 작업이 서로를 기다리며 멈추는 이유
생산자와 소비자를 동시에 실행하면 데이터가 자연스럽게 흘러갈 것 같다. 그런데 두 프로세스가 살아 있고 클록도 계속 뛰는데, 출력은 더 이상 나오지 않는 경우가 있다. DATAFLOW deadlock을 이해하려면 연산 속도보다 먼저 물어야 할 것이 있다. 각 프로세스는 지금 무엇을 기다리고 있으며, 그 조건을 만들어 줄 상대는 어디에서 멈췄을까?
DATAFLOW는 여러 작업을 겹쳐 실행할 기회를 만든다. 유한한 채널을 무한한 저장 공간으로 바꾸지는 않는다. blocking write는 채널이 가득 차면 기다리고, blocking read는 비어 있으면 기다린다. 한쪽의 기다림이 상대의 진행을 막고 그 상대가 다시 첫 번째 작업을 기다리면 순환이 닫힌다. AMD의 교착 분석 자료도 채널 연결이 같더라도 접근 순서에 따라 필요한 FIFO 깊이가 달라짐을 보여 준다. AMD, Deadlock Detection and Analysis의 Case 1·2
두 채널 X와 Y를 둔 작은 가상 모델을 만들어 보자. 생산자는 X에 네 값을 모두 쓴 다음 Y에 네 값을 쓴다. 소비자는 X 한 값, Y 한 값을 번갈아 읽는다. 각 읽기·쓰기는 원자적으로 끝나며, 가능한 작업은 결국 실행되고, 소비한 값은 별도의 무제한 출력에 기록한다고 가정한다. 프로세스 시작을 묶는 추가 제어 신호는 없다.
생산자: write X1, X2, X3, X4; write Y1, Y2, Y3, Y4
소비자: read X1; read Y1; read X2; read Y2; ...
채널 용량: X = 2, Y = 1
초기 상태: 두 채널 모두 비어 있음
소비자가 X1을 읽고 Y1을 기다린다고 하자. 생산자는 X2와 X3까지 쓸 수 있지만, 이제 X가 가득 차서 X4를 쓰지 못한다. Y1 쓰기는 X4 다음에 있으므로 아직 실행할 수 없다. 소비자는 Y1이 없어서 멈추고, 생산자는 소비자가 X를 더 비워 주지 않아서 멈춘다. 둘 다 다음 명령을 수행할 수 없다. 클록을 천 번 더 제공해도 이 상태에서 필요한 사건은 생기지 않는다.
여기서 X 용량을 4로 잡으면 생산자는 소비자의 읽기 진행과 관계없이 X 네 값을 저장할 수 있다. 따라서 Y1 쓰기에 도달할 수 있고, 이후 소비자가 X와 Y를 번갈아 비우며 생산자도 남은 Y 값을 쓴다. 이 모델에서 4는 진행을 보장하는 충분한 용량이다. 최소 용량이라고 주장한 것은 아니다. 실제 HLS 회로의 최소 깊이는 프로세스 제어, 파이프라인 내부 보관 공간, 읽기·쓰기 시점까지 포함해 확인해야 한다. 위의 명령 추적을 특정 도구의 합성 결과로 받아들이면 안 된다.
또 다른 해결은 생산자의 접근 순서를 X1, Y1, X2, Y2처럼 소비자와 맞추는 것이다. 같은 가상 모델에서는 두 채널의 용량이 각각 1이어도 진행할 수 있다. 다만 실제 알고리즘이 모든 X를 먼저 계산해야 Y를 만들 수 있다면 순서를 마음대로 바꿀 수 없다. 버퍼는 그 시간 차이를 저장 공간으로 흡수한다. 깊이를 줄이려면 저장 비용뿐 아니라 데이터가 만들어지는 의존성도 함께 바뀌어야 한다.
그렇다고 모든 교착에 큰 FIFO가 답인 것은 아니다. 작업 A는 B의 값을 먼저 읽고 나서 A의 값을 쓰며, 작업 B는 A의 값을 먼저 읽고 나서 B의 값을 쓴다고 하자. 두 채널이 처음에 비어 있으면 양쪽 모두 첫 읽기에서 멈춘다. 저장 공간을 백 개로 늘려도 빈 공간 안에서 첫 데이터가 저절로 만들어지지는 않는다.
이런 피드백 구조에는 알고리즘에 맞는 초기 상태가 필요할 수 있다. CIRCT의 Handshake 버퍼 표현은 순차 버퍼에 리셋 초기값을 명시하는 기능을 제공한다. 이는 값을 담을 자리와 시작에 실제 존재하는 토큰이 서로 다른 설계 정보라는 점을 잘 보여 준다. 아무 값이나 넣어 실행만 시작하면 계산 의미가 바뀔 수 있으며, 일반 HLS FIFO에 초기 토큰이 자동 생성된다는 뜻도 아니다. CIRCT Handshake, handshake.buffer
진행 여부와 처리율도 구분해야 한다. 외부 조건이 매 주기 두 토큰의 생산과 한 토큰의 소비를 계속 요구한다면 미처리량은 주기마다 하나씩 늘어난다. 유한한 버퍼는 정체가 발생할 시간을 늦출 수 있지만 그 요구를 무한히 만족시킬 수는 없다. 생산을 멈출 수 있는 계약인지, 소비를 병렬화할 수 있는지, 한 작업의 생산·소비 비율을 잘못 정했는지 살펴야 한다.
DATAFLOW 회로가 멈추면 먼저 멈춘 읽기와 쓰기를 연결해 보자. 가득 찬 채널 앞에서 쓰기가 막혔다면 burst 순서와 깊이가 단서가 된다. 빈 채널의 읽기만 서로 기다린다면 초기 토큰과 제어 의존성이 단서가 된다. 이 구분을 한 뒤 파형으로 실제 대기 관계를 확인하면, 버퍼를 늘려야 하는 이유와 늘려도 풀리지 않는 이유가 함께 보인다.