AMD MI300X에서 블록체인 그래프 분류를 위한 Qwen2-VL 파인튜닝: 공식 문서가 알려주지 않는 것들
요약
AMD MI300X 하드웨어 환경에서 Qwen2-VL 모델을 사용하여 블록체인 트랜잭션 그래프를 분류하는 파인튜닝 과정을 다룹니다. 텍스트 기반 LLM의 한계를 극복하기 위해 그래프를 이미지로 렌더링하여 시각적 위상 정보를 학습하는 방법론을 제시합니다.
핵심 포인트
- 블록체인 위상 정보를 포착하기 위해 텍스트 대신 시각(Vision) 데이터 활용
- AMD MI300X 및 ROCm 환경에서의 파인튜닝 및 vLLM 서빙 최적화
- Qwen2-VL의 고해상도 이미지 처리 능력을 활용한 그래프 세부 사항 보존
- LoRA와 Full-Parameter 학습 방식의 비교 및 ROCm 특유의 진단 방법
요약 (TL;DR): 블록체인 트랜잭션의 그래프 렌더링은 토큰 시퀀스로 직렬화하기 어려운 위상(Topology) 신호를 담고 있습니다. 47개의 수명이 짧은 리프(Leaf) 지갑에 둘러싸인 허브(Hub) 노드는 텍스트 형식으로는 주소와 금액의 테이블처럼 보이며, 패턴을 이미 알고 있는 경우에만 식별이 가능합니다.
📖 읽기 시간: 약 23분
이 글의 내용
- 문제점: 블록체인 포렌식에는 텍text가 아닌 시각(Vision)이 필요함
- MI300X에서의 하드웨어 및 환경 설정
- 데이터 파이프라인: 블록체인 그래프를 학습용 이미지로 렌더링하기
- 파인튜닝 (Fine-Tuning) 루프: 7B 모델에 대한 LoRA 적용 vs 7B 모델 전체 파라미터 (Full-Parameter) 학습 비교
- ROCm 특유의 실패 모드 및 진단 방법
- 추론 서빙 (Inference Serving): 분류 처리량(Throughput)을 위한 ROCm 기반 vLLM 활용
- 결론: 이 설정이 유효한 경우와 그렇지 않은 경우
문제점: 블록체인 포렌식에는 텍스트가 아닌 시각(Vision)이 필요함
블록체인 트랜잭션의 그래프 렌더링은 토큰 시퀀스로 직렬화하기 어려운 위상 (Topology) 신호를 담고 있습니다. 47개의 수명이 짧은 리프 (Leaf) 지갑에 둘러싸인 허브 (Hub) 노드는 텍스트 형식으로는 주소와 금액의 테이블처럼 보이며, 패턴을 이미 알고 있는 경우에만 식별이 가능합니다. 이미지로 렌더링되면, 해당 별 모양의 위상 (Star topology)은 구조적 형상으로서 즉시 눈에 띕니다. 이는 믹싱 (Mixing) 작업의 레이어링 패턴(자금이 시각적으로 뚜렷한 띠를 형성하는 순차적 깊이 레벨을 통해 이동하는 경우)과, 밀집된 내부 엣지(Edge)를 가진 주소 그룹이 희소한 외부 엣지와 대비되는 클러스터링 (Clustering) 시그니처에도 동일하게 적용됩니다. 시각-언어 모델 (Vision-language model)은 이러한 형상을 직접 분류하도록 학습할 수 있습니다. 트랜잭션 목록을 기반으로 작동하는 텍스트 기반 LLM은 원시 숫자로부터 위상 (Topology)을 재구성해야 하는데, 이는 가능하지만 취약합니다. 엣지 수 (Edge count)와 클러스터링 계수 (Clustering coefficient)를 계산하여 토큰으로 주입할 수는 있지만, 이는 시각 모델이 스스로 학습할 수 있는 기능을 사용자가 직접 피처 엔지니어링 (Feature engineering) 하는 것과 같습니다.
Qwen2-VL이 GNN (Graph Neural Network) 대신 이번 실험에 참여하게 된 이유는 주로 실용적인 측면 때문입니다. 그래프 신경망 (GNN)은 그래프 분류 (Graph classification)를 위한 학술적으로 올바른 도구이지만, 고정된 스키마 (Fixed-schema)의 그래프 데이터셋이 필요하며 일반적인 추론 (Reasoning) 작업에 사용하는 것과는 별개의 훈련 파이프라인 (Training pipeline)을 요구합니다. Qwen2-VL의 7B 변체는 Naive Dynamic Resolution 메커니즘을 통해 네이티브 고해상도 이미지 입력을 처리합니다. 즉, 모든 것을 고정된 그리드 (Grid)로 크기 조정하지 않고, 이미지가 들어온 해상도 그대로 이미지 타일 (Image tiles)을 토큰화합니다. 이는 엣지 (Edge)의 세부 사항을 보존하기 위해 트랜잭션 그래프 (Transaction graphs)를 1024×1024 이상의 해상도로 렌더링할 때 매우 중요합니다. 7B와 72B 변체 모두 오픈 웨이트 (Open-weight)이며, LLaMA-Factory 및 ms-swift를 통해 훈련 루프 (Training loop)를 처음부터 다시 구현할 필요가 없는 문서화된 파인튜닝 (Fine-tuning) 경로가 존재합니다. 이러한 조합 — 즉, 유능한 비전 인코더 (Vision encoder), 오픈 웨이트, 그리고 작동하는 파인튜닝 툴체인 (Toolchain) — 이 바로 이 실험을 단순히 논의하는 것에 그치지 않고 실제로 시도해 볼 만한 합리적인 대상으로 만드는 요소입니다.
문제는 시중에 나와 있는 모든 파인튜닝 가이드가 CUDA를 가정하고 있다는 점입니다. LLaMA-Factory의 문서, ms-swift의 퀵스타트 (Quickstart), Qwen2-VL 모델 카드 예제 등 모든 것이 nvidia-smi가 작동하는 NVIDIA GPU를 전제로 작성되었습니다. ROCm 6.x 기반의 AMD MI300X는 정당한 고용량 VRAM 컴퓨팅 환경 (전체 카드 기준 192GB HBM3)이지만, 운영 환경이 다릅니다. 라이브러리 지원 측면에서 ROCm의 PyTorch 빌드는 CUDA 빌드보다 뒤처져 있으며, Flash Attention은 ROCm에 맞춰 컴파일된 flash-attn이 필요하거나 빌드 마찰이 있는 xformers로 폴백 (Fallback)해야 합니다. 또한 CUDA는 무시하는 환경 변수들(ROCR_VISIBLE_DEVICES, HSA_OVERRIDE_GFX_VERSION)이 시작 스크립트의 핵심적인 부분(Load-bearing parts)이 됩니다. 이러한 문서의 공백은 이론적인 문제가 아닙니다. A100에서는 잘 작동하는 훈련 스크립트를 실행했을 때, 커널 디스패치 (Kernel dispatch)가 해결되지 않아 MI300X에서 조용히 CPU 연산으로 폴백되는 상황이 발생할 때 즉각적으로 나타납니다.
이 글에서 실제로 다루는 내용: 제가 실행한 파인튜닝 (fine-tuning) 루프, 진단하는 데 시간을 잡아먹었던 구체적인 ROCm 마찰 지점들, 그리고 벤치마크 보고서가 아닌 개발 로그 형식으로 정리한 처리량 (throughput) 및 분류 정확도 (classification accuracy)에 대한 솔직한 특성 분석입니다. 여기에는 완성된 제품은 없습니다. 목표는 동일한 스택을 시도하려는 사람에게 제가 이미 겪었던 실패 모드 (failure modes)를 건너뛸 수 있도록 충분한 세부 정보 (ROCm 버전, 라이브러리 버전, 설정 파일 내용, 에러 메시지)와 함께 실제 문제가 어디에 있는지에 대한 지도를 제공하는 것입니다.
MI300X에서의 하드웨어 및 환경 설정
MI300X의 192GB HBM3 풀은 단일 노드에서 무엇이 실행 가능한지에 대한 당신의 가정을 가장 먼저 재정립하게 만듭니다. Qwen2-VL과 같은 7B 모델의 전체 파라미터 파인튜닝 (Full-parameter fine-tuning)은 옵티마이저 상태 (optimizer states)와 그래디언트 (gradients)를 포함하더라도 80GB 미만에서 여유롭게 작동합니다. 즉, 메모리를 아껴 써야 하는 것이 아니라 연산 스케줄링 (compute scheduling)을 고민해야 한다는 뜻입니다. 72B 모델에 대한 LoRA는 다중 GPU 조정 문제라기보다 정당한 단일 노드 워크플로우가 됩니다. 실질적인 변화는 이렇습니다: 병목 현상이 "이게 들어갈까?"에서 "연산 유닛 (compute units)을 포화 상태로 유지하고 있는가?"로 이동합니다. 이는 더 나은 고민거리이지만, 데이터 로딩이나 토큰화 (tokenization)를 기다리느라 계속 대기하는 단순한 훈련 루프는 메모리 프로파일링 (memory profiling)에서 명확하게 드러나지 않는 방식으로 하드웨어를 유휴 상태로 만들 것이라는 의미이기도 합니다.
Qwen2-VL의 플래시 어텐션 (Flash Attention) 경로가 올바르게 작동하려면 ROCm 6.1이 최소 사양입니다. 작동 가능한 환경을 구축하는 가장 빠른 방법은 일반적인 Ubuntu 설치 위에 ROCm을 계층화하려고 시도하는 대신, AMD에서 유지 관리하는 Docker 베이스 이미지를 사용하는 것입니다:
docker pull rocm/pytorch:rocm6.1_ubuntu22.04_py3.10_pytorch_2.1.2
docker run --device=/dev/kfd --device=/dev/dri \
...
--ipc=host 플래그가 여기서 중요합니다. 이 플래그를 생략하면 멀티 워커(multi-worker) DataLoader 초기화 중에 훈련이 중단된 것처럼 보이는 무음 행(silent hangs) 현상이 발생합니다. 베이스 이미지를 사용하면, 순정 Ubuntu 이미지 위에 rocm-dev 패키지를 수동으로 설치할 때 발생하는 커널 헤더 버전 충돌을 해결할 필요 없이, 작동 가능한 HIP 스택, ROCm 인식 PyTorch, 그리고 집합 연산(collective ops)을 위한 적절한 RCCL 빌드를 확보할 수 있습니다.
다음 세 가지 환경 변수는 연산(ops)이 실제로 GPU에서 실행될지, 아니면 조용히 CPU로 폴백(fallback)될지를 제어합니다:
HIP_VISIBLE_DEVICES— HIP가 인식하는 GPU 인덱스를 제어합니다.CUDA_VISIBLE_DEVICES와 유사하지만, 모든 ROCm 버전에서 서로 깔끔하게 별칭(alias)으로 매핑되지는 않습니다.ROCR_VISIBLE_DEVICES— HIP 아래의 런타임 계층에서 작동합니다. 만약 이 값이HIP_VISIBLE_DEVICES와 일치하지 않으면, 연산(ops)이 일관성 없게 라우팅됩니다.HSA_OVERRIDE_GFX_VERSION=9.4.2— 컴파일된 커널에 아직 명시적인 gfx942 항목이 없는 경우, gfx942(MI300X가 보고하는 값)를 위해 필요합니다. 이 설정이 없으면 PyTorch는 조용히 범용 경로(generic path)나 CPU로 폴백(fallback)합니다.
이 중 하나라도 누락되었을 때 발생하는 실패 모드는 매우 교활합니다. 충돌도 없고, 경고도 없으며, 훈련은 계속 진행되고, 손실 곡선(loss curves)도 그럴싸해 보입니다. 그러다 처리량(throughput)이 예상보다 20배 느리거나, 프로파일링을 통해 모든 행렬 곱셈(matmul) 시간이 CPU에서 소요되고 있음을 발견했을 때 비로소 문제가 있음을 알게 됩니다. 이 변수들을 잊어버릴 수 있는 셸 내보내기(shell exports) 방식이 아니라, 컨테이너 엔트리포인트(entrypoint)나 훈련 실행 스크립트에 명시적으로 설정하십시오:
# .bashrc에만 추가하지 말고, 훈련 실행 스크립트에 추가하세요
export HIP_VISIBLE_DEVICES=0
export ROCR_VISIBLE_DEVICES=0
...
ROCm 환경에서의 Flash Attention 2는 별도의 주의가 필요합니다. 오류 없이 설치되는 flash-attn pip wheel은 CUDA를 기반으로 빌드되었습니다. 이는 ROCm 시스템에서도 깔끔하게 설치되고 불만 없이 임포트(import)되지만, 학습 중에 잘못된 그래디언트 (gradients)를 생성합니다. NaN(Not a Number)이나 발산(explosion)이 발생하는 것이 아니라, 파인튜닝 (fine-tune)을 망가뜨리는 미세하게 잘못된 값들이 생성됩니다. 이를 해결하기 위한 두 가지 정당한 방법은 다음과 같습니다: ROCm 스택에 맞춰 flash-attn을 소스에서 직접 빌드하거나 (시간은 오래 걸리지만 확실한 방법), 별도의 패키지 없이도 HIP에서 네이티브하게 작동하며 aotriton 기반의 Flash Attention 구현을 제공하는 PyTorch 2.3+ 버전을 사용하는 것입니다. 만약 2.1.2 베이스 이미지(base image)를 사용 중이라면, 소스 빌드를 계획하거나 파인튜닝을 시작하기 전에 컨테이너 내부의 PyTorch 레이어를 업그레이드하는 것을 고려하십시오.
# 실제로 어떤 어텐션 (attention) 백엔드가 사용되고 있는지 확인
# 검증을 위해 모델 초기화 코드에 이를 추가하세요
import torch
...
데이터 파이프라인: 블록체인 그래프를 학습용 이미지로 렌더링하기
그래프-이미지 렌더링 선택은 생각보다 훨씬 중요합니다
렌더링 파이프라인은 대부분의 블록체인 그래프 분류 프로젝트가 학습이 시작되기도 전에 조용히 성능을 잃게 되는 지점입니다. 512×512 해상도에서 NetworkX + Matplotlib을 사용하는 것은 분명 명확한 시작점입니다. 코드 세 줄이면 충분하며 수백 개의 노드(nodes)까지 문제없이 처리합니다. 문제는 규모가 커질 때 나타납니다. 에지 가중치 (edge weights)가 시각적 노이즈로 에일리어싱 (aliased)되고, 차수가 높은 노드 (high-degree nodes)들은 검은 덩어리로 뭉쳐지며, 500개 이상의 에지를 가진 그래프는 인간 분석가조차 읽을 수 없는 구별 불가능한 얼룩이 되어버립니다. 이미지가 인간에게도 모호하다면, 비전 모델 (vision model)에게도 매우 어려울 것입니다.
힘 지향 레이아웃 (force-directed layouts)으로 전환하는 것 — 구체적으로 Gephi의 ForceAtlas2를 SVG로 내보낸 후, cairosvg 또는 Inkscape의 headless CLI를 사용하여 1024×1024로 래스터화 (rasterized) 하는 방식 — 은 초기 평가 실행에서 클래스 분리성 (class separability)을 눈에 띄게 개선하였으며, 이는 훈련 우선순위를 변경할 정도로 충분했습니다. SVG 중간 단계가 중요한 이유는 다음과 같습니다: 어떤 내보내기 해상도에서도 손실 없는 기하학적 구조 (lossless geometry)를 얻을 수 있으며, 래스터화하기 전에 Matplotlib의 레이아웃 엔진과 씨름하는 대신 Gephi 설정에서 노드 크기, 에지 불투명도 (edge opacity), 색상 인코딩 (color encoding)을 조정할 수 있기 때문입니다. 파이프라인에 최종적으로 포함되는 명령어는 대략 다음과 같습니다:
# Headless Inkscape SVG → 1024×1024 PNG
inkscape --export-type=png \
--export-filename=output.png \
...
완전 자동화가 필요한 그래프 (Gephi GUI를 사용할 수 없는 경우)의 경우, Python graphviz 패키지를 통해 sfdp 엔진을 사용하는 graphviz가 프로그래밍 방식으로 힘 지향 레이아웃을 처리하며, 동일한 노드 수에서 NetworkX의 spring layout보다 더 깔끔한 SVG를 생성합니다. 트레이드오프(trade-off)는 다음과 같습니다: sfdp 레이아웃은 그래프당 NetworkX의 spring_layout보다 느리며, 밤새 수천 개의 그래프를 렌더링하는 배치 작업 (batch jobs)의 경우 이 실행 시간(runtime)이 누적됩니다.
레이블 스키마 (Label Schema): 시각적 구별 문제
네 가지 클래스 스키마 — 일반 전송 클러스터 (normal transfer cluster), 믹서/텀블러 토폴로지 (mixer/tumbler topology), 거래소 핫월렛 팬아웃 (exchange hot-wallet fan-out), DeFi 컨트랙트 상호작용 웹 (DeFi contract interaction web) — 는 그래프 구조가 단순히 의미론적으로(semantically) 구별될 뿐만 아니라, 올바르게 렌더링되었을 때 시각적으로도 뚜렷하게 구분되기 때문에 선택되었습니다. 사실 두 번째 제약 조건이 더 까다로운 부분입니다. 믹서/텀블러 토폴로지는 소수의 중간 노드에 높은 매개 중심성 (betweenness centrality)을 가진 특징적인 다대일대다 (many-to-one-to-many) 구조를 가집니다. 거래소의 핫월렛 팬아웃은 하나의 중심 노드로부터 높은 외차수 (out-degree)를 가진 스타 그래프 (star graph) 형태를 띱니다. DeFi 상호작용 웹은 컨트랙트 주소들로부터 밀집된 삼각형 클러스터링 (triangle-clustering)을 보입니다. 만약 렌더링 방식이 이러한 구조적 차이를 뭉개버린다면 — 모든 주소 유형에 동일한 노드 색상을 사용하거나, 엣지 가중치 (edge-weight) 인코딩이 없거나, 대비가 낮다면 — 모델에게 이질적인 레이블을 가진 시각적으로 균일한 이미지를 통해 학습하라고 요구하는 셈이며, 이는 모델이 일반화(generalize)하기보다 암기(memorize)하게 만드는 지름길입니다.
내보내기 전에 색상으로 노드 유형(EOA 주소 vs 컨트랙트 주소 vs 거래소 태그 주소)을 인코딩하고 선 두께로 엣지 가중치를 인코딩하는 것은 모델이 실제로 사용할 수 있는 의미 있는 신호를 추가합니다. 이를 위해서는 그래프 객체가 해당 속성들을 포함하고 있어야 합니다. NetworkX에서는 다음과 같습니다:
import networkx as nx
G = nx.DiGraph()
...
ms-swift 파인튜닝을 위한 데이터셋 형식
ms-swift를 통한 Qwen2-VL 파인튜닝은 각 줄이 독립적인 학습 예시인 JSON-L 형식을 요구합니다. 중요한 두 가지 키는 image와 conversations입니다. image 필드는 로컬 경로, URL 또는 base64로 인코딩된 문자열을 허용합니다. 로컬 경로가 편리해 보일 수 있지만 실제 주의해야 할 점(gotcha)이 있습니다: ms-swift는 실행 시점의 작업 디렉토리를 기준으로 상대 경로를 해석합니다. 만약 다른 셸이나 Docker 컨테이너에서 체크포인트로부터 학습을 재시작할 경우, 상대 경로는 조용히 잘못된 위치로 해석되어 데이터셋이 로드되는 것처럼 보이지만 실제로는 모델에 빈 이미지를 공급하게 됩니다. 절대 경로를 사용하면 이 문제를 완전히 피할 수 있습니다.
{"image": "/data/blockchain_graphs/train/mixer_0041.png", "conversations": [{"role": "user", "content": "\n보여지는 트랜잭션 그래프 토폴로지 (topology)를 분류하세요."}, {"role": "assistant", "content": "mixer_tumbler"}]}
{"image": "/data/blockchain_graphs/train/hotspot_0012.png", "conversations": [{"role": "user", "content": "\n보여지는 트랜잭션 그래프 토폴로지 (topology)를 분류하세요."}, {"role": "assistant", "content": "exchange_hot_wallet_fanout"}]}
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기