---
title: "덧셈은 맞는데 쌍이 틀렸다, stream join의 수락 조건"
author: "브레이브브라우저쓰자"
published_at: "2026-10-08T01:31:04.751Z"
updated_at: "2026-10-05T14:08:17.810Z"
tags: ["stream join"]
canonical_url: "https://ariser.io/@writer_a9817d5a8de0/%EB%8D%A7%EC%85%88%EC%9D%80-%EB%A7%9E%EB%8A%94%EB%8D%B0-%EC%8C%8D%EC%9D%B4-%ED%8B%80%EB%A0%B8%EB%8B%A4-stream-join%EC%9D%98-%EC%88%98%EB%9D%BD-%EC%A1%B0%EA%B1%B4"
ai_assisted: true
---

# 덧셈은 맞는데 쌍이 틀렸다, stream join의 수락 조건

이 글은 AI가 활용됐습니다.

[원문](https://ariser.io/@writer_a9817d5a8de0/%EB%8D%A7%EC%85%88%EC%9D%80-%EB%A7%9E%EB%8A%94%EB%8D%B0-%EC%8C%8D%EC%9D%B4-%ED%8B%80%EB%A0%B8%EB%8B%A4-stream-join%EC%9D%98-%EC%88%98%EB%9D%BD-%EC%A1%B0%EA%B1%B4)

두 입력을 더하는 회로에서 값 하나가 잘못 짝지어졌다. A 쪽 입력이 먼저 도착했을 때 포인터를 넘겼고, 한 사이클 뒤 B가 도착하자 A의 다음 값과 더했다. 덧셈기 자체는 정확하다. **stream join은 여러 입력 스트림의 현재 항목을 하나의 출력 거래로 묶는 동기화 회로**다. 연산이 맞아도 어느 항목끼리 연산하는지가 틀리면 결과는 틀린다.

여기서는 A0와 B0, A1과 B1이 각각 한 쌍이라고 정한다. 두 입력은 같은 순서로 항목을 만들지만 도착 시각은 다를 수 있다. 생산자는 수락 전까지 valid와 payload를 유지하고, 모두 같은 클록 에지에서 거래를 센다. 출력은 두 값의 합이며 결과 폭은 손실 없이 충분하다고 가정한다.

## 먼저 도착한 항목을 먼저 소비하면 안 되는 이유

입력 ready를 출력 ready에 그대로 연결하는 잘못된 구조를 생각하자.

```systemverilog
assign valid_out = valid_a && valid_b;
assign ready_a   = ready_out;
assign ready_b   = ready_out;
```

에지 0에 A0는 valid=1이고 B0는 아직 valid=0이라고 하자. ready_out=1이면 ready_a도 1이다. A 생산자는 A0를 수락한 것으로 알고 다음 A1로 넘어간다. 그러나 valid_out=0이므로 출력 거래는 없었다. 어디에도 A0를 저장하지 않았다면 A0는 사라졌다.

에지 1에 B0가 도착하고 A1도 준비되어 있으면 출력은 A1+B0가 된다. 입력별 전송 개수는 정상처럼 보일 수 있지만, 쌍의 의미가 바뀌었다. 이 문제는 값의 진리표가 아니라 입력 수락과 출력 수락을 함께 추적해야 발견할 수 있다.

## 둘 다 있을 때 같은 에지에 넘기기

저장 없는 최소 join에서는 모든 항목이 준비되고 출력이 수락될 때 입력도 함께 수락하게 할 수 있다.

```systemverilog
assign valid_out = valid_a && valid_b;
assign ready_a   = valid_out && ready_out;
assign ready_b   = valid_out && ready_out;
assign sum_out   = {1'b0, data_a} + {1'b0, data_b};
```

data_a와 data_b가 각각 8비트 unsigned이면 sum_out은 9비트로 둔다. 앞에 0을 붙여 0~510의 합을 표현하는 설명용 예제다. valid_out이 0일 때 sum_out은 의미 없는 값일 수 있다. 소비자는 valid_out과 ready_out이 함께 높은 에지의 결과만 받는다.

이 구조에서는 출력 fire와 두 입력 fire가 같은 조건에서 성립한다. A가 먼저 도착하면 A를 유지한다. B도 도착했지만 출력이 막히면 둘 다 유지한다. 출력이 받는 에지에 두 항목의 책임이 동시에 끝난다.

가상의 숫자로 보면 관계가 명확하다. A0=3, A1=7, B0=8이라고 하자. A0만 있는 에지 0에서는 두 ready가 0이어서 A0를 보존한다. 에지 1에 B0가 도착하고 출력이 ready라면 3+8=11을 수락한다. 다음 쌍으로 넘어가는 것은 그 에지 이후다. 잘못된 구조의 7+8=15와 차이가 생긴 위치는 덧셈이 아니라 A0의 조기 수락이다.

[PULP의 stream join](https://github.com/pulp-platform/common_cells/blob/master/src/cc_stream_join.sv)과 그 [동적 입력 선택 구현](https://github.com/pulp-platform/common_cells/blob/master/src/cc_stream_join_dynamic.sv)은 선택한 입력들이 모두 유효할 때 출력과 입력을 함께 수락시키는 사례다. payload는 별도 경로로 전달한다. 위 덧셈 코드는 그 제어 원리를 설명하기 위한 별도의 예제다.

## ready를 기다리는 생산자는 여기서 멈춘다

join은 다른 입력의 도착을 기다리기 위해 ready를 낮출 수 있다. 두 생산자가 모두 ready를 보고 나서 valid를 올리는 구조라면, join은 valid를 기다리고 생산자는 ready를 기다린다. 둘 다 값이 준비되어 있어도 전달이 시작되지 않을 수 있다.

따라서 이 최소 구조에서는 생산자가 수락 여부와 독립적으로 준비된 값을 제시하고, 수락 전까지 유지해야 한다. ready가 “이제 데이터를 만들어라”는 요청이 아니라 “제시한 항목을 이 에지에 받을 수 있다”는 수락 정보라는 구분이 중요하다. 버퍼가 있어도 모듈마다 제시 규칙이 다르면 진행 조건을 다시 확인해야 한다.

[CIRCT의 Handshake 문서](https://circt.llvm.org/docs/Dialects/Handshake/)는 join을 모든 입력이 준비되었을 때 유효한 출력을 만드는 동기화 연산으로 설명한다. 이런 데이터 흐름 모델에서도 연산자는 값만 소비하는 것이 아니라, 필요한 토큰의 준비를 함께 기다린다.

## 큐를 추가해도 '어떤 쌍인가'는 자동으로 알지 못한다

각 입력 앞에 큐를 두면 먼저 도착한 A0를 저장하고 상류를 더 진행시킬 수 있다. join은 두 큐의 맨 앞 항목을 함께 소비한다. 앞의 조기 수락이 올바른 버퍼링으로 바뀌는 것이다. 그러나 큐가 있다고 항목의 의미까지 정렬되는 것은 아니다.

A 스트림에서 항목 하나를 생략했는데 B 스트림에는 그대로 있다면, 단순한 FIFO 순서의 join은 그 뒤부터 잘못된 쌍을 계속 만든다. 거래 식별자가 있는 경우에는 두 식별자가 같은지 확인해야 할 수 있다. 불일치했을 때 정지할지 오류를 낼지, 한쪽을 버리거나 다시 맞출지는 상위 계약으로 정한다. join이 단순히 nth 항목을 nth 항목과 묶는지, 식별자로 짝을 찾는지도 다르다.

배열 연산의 좌표나 분기 후 합류하는 제어 토큰처럼, 두 스트림이 같은 논리 작업을 대표한다는 성질이 필요하다. 완료 순서가 바뀌는 생산자와 FIFO join을 연결하면 순서 보정이 별도로 필요할 수 있다. 제어 신호가 정상인 것과 의미가 같은 쌍을 고르는 것은 별도 검증 대상이다.

## 정렬 검증은 값과 수량을 함께 본다

검증에서는 한쪽만 먼저 도착하는 경우, 모두 준비되었는데 출력이 오래 막히는 경우, 연속해서 모든 입력이 준비되는 경우를 구분해 추적할 수 있다. 저장 없는 join이라면 출력 한 번에 각 입력 한 번이라는 수량 관계가 성립해야 한다. 저장이 있으면 입력 수락과 출력 사이의 차이는 해당 큐에 남은 항목 수로 설명되어야 한다.

그 위에서 A0와 B0가 실제로 한 작업인지 식별자나 독립된 기대 결과로 확인한다. 덧셈 결과가 우연히 같아도 잘못된 쌍을 고른 사실은 사라지지 않는다. 숫자만 비교하는 테스트에서 빠지는 정렬 정보를 남기는 이유다.

처음의 틀린 합은 두 생산자의 속도가 달라서 필연적으로 생긴 것이 아니다. **stream join이 모든 피연산자의 준비와 결과 수락을 한 거래로 묶지 못해서 생긴 것**이다. 도착 시각이 달라도 쌍의 책임을 함께 끝내고, 순서 또는 식별자의 계약을 유지하면 서로 다른 stall을 겪는 입력도 정확한 한 결과로 합칠 수 있다.

