본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 06. 23. 21:57

확산 LLM DiffusionGemma를 Modal로 구동하고, 노이즈 제거 중간 과정을 브라우저로 시각화해 보았다

요약

Google의 DiffusionGemma를 Modal 클라우드 GPU 환경에서 구동하고, 노이즈 제거(Denoising) 과정을 브라우저에 실시간 시각화하는 기술적 구현 방법을 소개합니다. SSE(Server-Sent Events)를 활용해 모델의 생성 단계를 스트리밍하며 시각적으로 확인하는 아키텍처를 다룹니다.

핵심 포인트

  • DiffusionGemma의 블록 자기회귀(Block-autoregressive) 방식 이해
  • Modal을 활용한 A100 GPU 인프라 구축 및 비용 효율적 운영
  • SSE를 이용한 모델의 디노이징 중간 과정 실시간 시각화 구현
  • 대규모 모델 추론을 위한 클라우드 GPU 및 서버 아키텍처 구성

2026년 6월, Google이 DiffusionGemma를 공개했습니다. 기존의 Gemma와 달리, 자기회귀(Autoregressive, 왼쪽에서부터 1토큰씩) 방식이 아니라, 확산(Diffusion) 기반으로 텍스트를 생성하는 LLM(dLLM)입니다. 이미지 생성에서 주류가 된 확산 모델(Diffusion Model)의 개념을 언어 생성에 도입한 것입니다.

이번에는 이를 실제로 구동하여 다음 두 가지를 시도해 보았습니다.

  • 노이즈 제거(Denoising)의 중간 과정을 브라우저에서 시각화하기
  • 양방향성이 효과적인 태스크(OCR)에서의 검증

"확산 LLM은 병렬로 생성한다"라고 자주 설명되지만, 글로 읽는 것보다 노이즈 가득한 상태가 깨끗한 문장으로 조각되어 가는 모습을 실제로 보는 것이 더 이해하기 쉽지 않을까? 라는 생각에, 우선 그것을 화면에 띄우기로 했습니다. 실행 환경으로는 클라우드 GPU인 Modal을 사용하고 있습니다.

확산 LLM이란

자기회귀 모델은 문장을 왼쪽에서부터 1토큰씩 순차적으로 생성합니다. 반면 확산 LLM은 고정 길이의 캔버스(토큰의 나열) 전체를 노이즈 가득한 상태로부터 반복적으로 노이즈 제거(Denoising)하여 완성합니다. 각 단계(Step)에서 모든 위치를 동시에 재검토할 수 있으므로, 원리적으로는 병렬로 빠르게 생성할 수 있습니다.

DiffusionGemma는 이 확산을 256토큰 블록 단위로 수행하며, 블록이 확정되면 다음 블록으로 넘어가는 블록 자기회귀(Block-autoregressive)적인 구성으로 되어 있습니다[1].

항목내용
모델google/diffusiongemma-26B-A4B-it
...

Modal로 구동하기

26B-A4B는 활성 파라미터(Active parameters)가 약 38억 개이지만, 추론 시에는 총 파라미터(약 252억 개)만큼의 가중치를 VRAM에 올려야 하므로, 가지고 있는 RTX 4070 Ti Super(16GB)에는 올라가지 않습니다. 그래서 필요한 때만 GPU를 빌려 쓸 수 있는 Modal의 A100을 사용했습니다.

참고로, 양자화(Quantization, 4bit) 로드는 이번 환경(transformers 5.11 계열)에서 실패했기 때문에 bf16 상태 그대로 올렸습니다[2].

import modal
app = modal.App("dgemma-web")
image = (
...

구성

브라우저로부터 입력을 받고, Modal 상의 모델이 생성하면서 디노이즈(Denois)의 중간 과정을 순차적으로 반환하는 구성입니다.

Modal 상에는 브라우저용 web(FastAPI / ASGI)과 GPU에서 모델을 구동하는 Model을 별도로 배치했습니다. web 측은 가볍고 상주에 가까운 반면, Model 측은 A100을 사용하므로 요청이 왔을 때만 기동하고, 한동안 액세스가 없으면 자동으로 정지합니다(그동안은 GPU 과금도 발생하지 않습니다).

흐름은 다음과 같습니다. 브라우저가 프롬프트를 /api/chat으로 POST하면, web이 Model의 생성을 호출합니다. Model은 블록을 디노이즈하면서 각 단계의 중간 캔버스(Draft)와 확정된 텍스트(Committed)를 이벤트로 내보냅니다. web은 이를 받아 SSE(Server-Sent Events)로 브라우저에 흘려보냅니다. 브라우저는 draft를 회색, committed를 검은색으로 그리므로, 노이즈가 걷히는 모습이 그대로 화면에 나타납니다.

Model과 web을 가로지르는 스트리밍은 Modal의 제너레이터(Generator)로 구현했습니다. Model 측의 생성 메서드를 제너레이터로 정의하고, web 측은 remote_gen.aio()로 이벤트를 하나씩 받아 SSE로서 재전송합니다.

실행 환경

  • GPU: NVIDIA A100-80GB (Modal)
  • 모델: google/diffusiongemma-26B-A4B-it (bf16 풀 로드)
  • 주요 라이브러리: unsloth, transformers 5.11, FastAPI

시각화 구현

디노이즈의 각 단계 중간 캔버스를 브라우저에 그대로 흘려보내 표시합니다.

드래프트(Draft)는 어디서 가져오는가

DiffusionGemma는 최종 텍스트 이전에 중간 드래프트를 생성합니다. transformers에는 전용 TextDiffusionStreamer가 준비되어 있어, 이를 generate에 전달하면 중간 상태를 볼 수 있습니다.

다만, 이 클래스의 소스 코드를 읽어보니 드래프트를 ANSI 이스케이프(ANSI escape)를 사용하여 터미널에 직접 print하고 있을 뿐이었습니다. put_draft에서 모든 드래프트 토큰 열을 받아 커서를 저장하고 노란색으로 표시한 뒤, 다음 단계에서 덮어쓰는 동작을 수행합니다. 즉, 표준 상태 그대로라면 터미널에는 출력되지만 브라우저로는 전달되지 않습니다.

그래서 TextDiffusionStreamer를 상속받아 put_draft를 오버라이드(override)하고, ANSI로 그리는 대신 큐(queue)로 흘려보내도록 했습니다. 확정된 텍스트는 on_finalized_text에서 가져옵니다.

from transformers import TextDiffusionStreamer
class QueueDiffusionStreamer(TextDiffusionStreamer):
    def __init__(self, tokenizer, out_q, **kw):
        ...

generate는 별도의 스레드에서 실행하며, 큐에 쌓인 이벤트를 제너레이터(generator)로 yield합니다.

또 다른 난관: 버퍼링 (Buffering)

여기까지 하면 동작해야 했지만, 초기 구현에서는 기다리는 동안 계속 공백이다가 마지막에 한꺼번에 모든 내용이 나오는 상태였습니다. 디노이즈(denoise)는 생성되고 있는데, 응답이 버퍼링되어 마지막에 몰아서 도착하고 있었습니다.

이는 전송 형식을 SSE (text/event-stream)로 바꾸고 순차적으로 플러시(flush)하도록 하여 해결했습니다.

@modal.asgi_app()
def web():
    from fastapi import FastAPI
    ...

브라우저 측은 확정된 텍스트(committed)를 검은색으로, 중간 단계의 캔버스(draft)를 회색으로 표시할 뿐입니다.

실행 결과: 디노이즈가 새겨지는 모습

이제 회색의 뭉개진 텍스트가 여러 번 바뀌면서 검은색 확정 문장으로 수렴해 가는 모습이 브라우저에 흐르게 되었습니다.

예를 들어 「쥬겐무(寿限無)」에 대해 물으면, 중간 단계에서는 다음과 같은 상태를 볼 수 있습니다 (실제 화면에서는 회색으로 표시).

寿無彦寿彦彦彦寿寿寿寿寿寿寿寿寿寿寿寿寿寿 ...
名前名前名前名前が名前
、が、、、、、、、、、、、、、

寿寿寿名前名前、、、、、와 같은 반복이나 플레이스홀더(placeholder) 투성이인 상태가 단계를 거듭할수록 깨끗한 문장으로 수렴해 갑니다. 전체를 한 번에 훑어보며 수정해 나가는, 확산(diffusion) 모델다운 움직임입니다.

DiffusionGemmaがノイズから文章を生成していく様子

짧은 대화에서의 생성 속도는 A100-80GB · bf16 · 1개 요청 기준 대략 58~122 tok/s였습니다 [3].

OCR: 양방향성이 효과적인 태스크로 테스트

확산 모델은 자기회귀(autoregressive) 모델과 달리 모든 위치를 동시에 (양방향으로) 볼 수 있다는 것이 특징입니다. 빈칸 채우기 같은 태스크에 적합할 것이라 판단하여 OCR로 테스트해 보았습니다.

검증을 위해 일부러 열화시킨 가상의 발주서 FAX(기울기, 노이즈, 스캔 불균일 추가)를 준비하고, "이 이미지의 텍스트를 읽어줘"라고 요청했습니다.

読み取らせた架空の発注書FAX

결과는 다음과 같습니다 (발주 번호, 전화번호, 검수란 등의 헤더 부분은 생략하고 주요 부분만 발췌). Markdown 표까지 재현해 냈습니다.

| No | 品名 | 数量 | 単価 | 金額 |
| 1 | ステンレス鋼板 SUS304 | 50 | 3,200 | 160,000 |
| 2 | アルミ角材A6063 | 120 | 850 | 102,000 |
...

생성 속도는 1844 tok / 4.0s = 459 tok/s였습니다.

OCR 중에도 확정된 블록은 검은색, 디노이즈 중인 블록은 회색으로 표시됩니다.

OCR中のデノイズの様子(上が確定、下が途中キャンバス)

정답과 대조해 보니 전체 20개 항목 중 19개 항목이 정답이었습니다. 숫자(발주 번호, 수량, 단가, 금액, 소계, 소비세, 합계, 전화, 날짜)는 모두 정확했고, 한자(鋼板, 角材, 六角, 防錆, 蒲田, 検収)도 모두 정확했습니다.

오류는 단 하나였습니다.

정답판독 결과
コウゾウカ技研株式会社コソウカ技研株式会社

틀린 것은 한자가 아니라, 가타카나로 된 가상의 사명이었습니다. 사전에 있는 한자 숙어는 정확하게 읽는 반면, 사전에 없는 조어인 가타카나 사명에서는 틀렸습니다. 아는 단어에는 강하지만, 모르는 고유 명사에는 약한 것으로 보입니다.

내용의 정확성

깔끔하게 생성된다고 해서 답이 맞다는 뜻은 아닙니다. 아까의 「쥬겐무」도 내용은 틀렸었습니다.

항목모델의 출력올바른 내용
읽기ことぶげんなくじゅげむ
풀네임寿限無、寿限無、彦戸屋、寿、寿…寿限無、寿限無、五劫の擦り切れ、海砂利水魚の…

문장 자체는 자연스럽기 때문에 언뜻 봐서는 오류를 알아차리기 어렵습니다.

또한, 확산 (Diffusion)은 블록 전체를 반복적으로 완성해 나가는 생성 방식으로, 샘플링 (Sampling)을 동반하기 때문에 동일한 입력이라도 실행할 때마다 결과가 달라질 수 있습니다. 이번 OCR은 거의 완벽했지만, 다른 기회에 동일한 종류의 장표를 읽게 했을 때는 한자를 몇 개 오독하거나 날짜를 하루 차이 나게 읽는 경우도 있었습니다. 잘 읽힐 때와 틀릴 때의 차이가 큽니다.

요약

DiffusionGemma (확산 LLM)를 Modal의 A100-80GB에서 구동하고, 노이즈 제거 (Denoise)의 중간 과정을 브라우저로 시각화하며 OCR까지 시도해 보았습니다.

알게 된 점:

  • 노이즈 제거의 중간 과정은 TextDiffusionStreamer의 put_draft를 가로채면 취득할 수 있으며, SSE로 순차적 전송을 하면 브라우저에 표시할 수 있다.
  • OCR은 숫자와 한자 모두 고정밀도였다 (이번에는 459 tok/s, 20개 항목 중 19개 정답).

과제:

  • transformers/unsloth의 4bit 로드는 이번 환경에서 실패했다 (로컬에서 구동한다면 GGUF 양자화 버전 + llama.cpp의 diffusion 대응 빌드를 사용한다).
  • 문장은 자연스러워도 내용을 틀린다 (고유명사 날조, 잘못된 읽기).
  • 생성할 때마다 결과가 흔들리며, 오류의 위치도 일정하지 않다.
  • 가타카나 미지의 고유명사 등 사전 외 단어에 약하다.

확산 LLM은 빠르고 병렬로 생성할 수 있는 반면, 자연스러운 문장이라도 내용이 틀릴 수 있다는 양면성을 실제로 접하며 확인할 수 있었습니다. 노이즈로부터 문장이 세워져 가는 모습은 몇 번을 봐도 흥미롭습니다.

링크

  • DiffusionGemma 모델 카드 (Hugging Face)
  • DiffusionGemma 개발자 가이드 (Google Developers Blog)
  • Transformers diffusion_gemma 문서
  • DiffusionGemma in vLLM (vLLM Blog)
  • GGUF 양자화 버전 (unsloth)
  • Modal

Discussion

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0