CUDA 가 사실상 표준이 된 시대에 Intel GPU 위에서 같은 일을 하는 길은 어떻게 생겼는가 — Patric Zhao 가 GPU Mode 첫 SYCL 강의에서 깐 이종 컴퓨팅 모델, 커널 작성, 메모리 모델, 그리고 LLM inference 에 적용한 fused operator 들의 학습 노트.
GPU Mode 시리즈는 이름부터 CUDA mode 로 출발했다. 그런데 25강을 넘어서면서 — Mark 가 이름을 굳이 GPU Mode 로 바꾼 다음 첫 외부 백엔드 강의로 — Intel 의 Patric Zhao 를 불렀다. 강의의 이름이 일부러 “SYCL mode” 다.
강의가 답하려는 질문은 두 줄로 압축된다.
그래서 강의의 구조도 정확히 그 두 축이다. 전반부는 SYCL 의 이종 컴퓨팅 모델(host + device, queue, accessor, parallel_for) 을 CUDA 의 친숙한 모델 위에 mapping 한다. 후반부는 Intel oneAPI 위에서 SYCL 로 짠 fused operator 들 — RMSNorm + matmul, flash attention, MHA fusion — 의 실측 결과를 보여준다.
SYCL 은 “C++ 표준 위에 single-source 로 GPU 커널을 짠다”는 약속의 결과물이다. CUDA 가 NVIDIA 의 독점적 ISA 위에 묶여 있다면, SYCL 은 vendor 가 만든 backend 만 있으면 같은 코드가 NVIDIA · AMD · Intel · CPU 위에서 모두 돈다는 위치에 서 있다. 즉 “이 GPU 의 프로그래밍 모델이 무엇인가” 가 아니라, 모든 가속기를 추상화한 한 layer 가 무엇인가 의 질문에 답한다.
그래서 강의의 끝에 손에 잡혀 있어야 할 것은 — SYCL 의 핵심 개념 4개(queue, parallel_for/handler, USM, accessor), Intel GPU 의 메모리/스레드 모델(EU, slice, SLM), 그리고 이 모델이 CUDA 와 어디서 다른가 의 한 페이지짜리 mapping 이다.
강의의 첫 30분은 의도적으로 “heterogeneous computing” 이라는 더 넓은 단어로 시작한다. host + device 의 분리된 메모리, latency hiding 을 위한 SIMT 모델, async dispatch — 이 다섯 가지는 아키텍처 선택과 무관하게 모든 가속기에 공통이다.
Patric 의 출발점은 — “GPU 라는 단어가 NVIDIA 와 동의어처럼 쓰이지만, 그 안에는 더 일반적인 모델이 있다” 는 것. 그 모델을 명시적으로 깐다.
이 frame 위에서 SYCL 이 등장하는 위치가 자연스러워진다 — 이 mapping 자체를 한 번에 묶는 single-source C++ layer. 같은 parallel_for 가 NVIDIA · Intel · AMD · CPU 위에서 backend 만 바꿔 돈다는 약속.
queue, handler, accessor/USM, parallel_forSYCL 의 spec 자체는 두꺼운 문서지만, 강의에서 Patric 이 잡은 진입점은 네 개의 추상이다. 이 넷만 알면 vector add 부터 GEMM 까지 다 짤 수 있다.
sycl::queue q{gpu_selector_v}.q.submit([&](handler& cgh){ ... }) 안에서 한 dispatch 의 dependency 와 launch 를 묶는다. command group.malloc_device · malloc_shared · malloc_host. CUDA 의 cudaMalloc / managed memory 와 같은 자리.nd_range<3>(global, local) 의 (global, local) 이 CUDA 의 (grid×block, block) 과 동치.item.get_global_id(0) · item.get_local_id(0). CUDA 의 blockIdx · threadIdx.// SYCL vector add — 강의의 첫 코드
#include <sycl/sycl.hpp>
using namespace sycl;
int main(){
constexpr int N = 1024;
queue q{gpu_selector_v};
// 1. USM 으로 device 메모리 할당
float* a = malloc_shared<float>(N, q);
float* b = malloc_shared<float>(N, q);
float* c = malloc_shared<float>(N, q);
// 2. submit + parallel_for
q.submit([&](handler& cgh){
cgh.parallel_for(range<1>{N}, [=](id<1> i){
c[i] = a[i] + b[i];
});
}).wait();
free(a, q); free(b, q); free(c, q);
}
CUDA 와 비교했을 때 — cudaMalloc 대신 malloc_shared, kernel<<<...>>> 대신 q.submit + parallel_for, cudaDeviceSynchronize() 대신 .wait(). 모양은 다른데 역할은 정확히 같은 자리를 차지한다.
malloc_device = device 만 접근 (cudaMalloc 동치). malloc_shared = host/device 양쪽 가시 (managed memory). malloc_host = pinned host (zero-copy 후보). 강의에서 Patric 이 첫 데모는 malloc_shared 로 깔끔하게 시작 — 그 다음 GEMM 같은 perf 코드는 malloc_device + 명시적 memcpy 로 옮겨간다.
slice, EU, sub-group, SLMSYCL 자체는 vendor 중립이지만, Intel GPU(Arc, Data Center GPU Max, Ponte Vecchio) 위에서는 Xe architecture 의 단어들을 다시 만난다. 이 자리에 익숙해져야 SYCL 커널의 launch 설정이 직관적으로 잡힌다.
Xe 아키텍처의 한 단계 더 — Data Center GPU Max(Ponte Vecchio) 의 단위 분해.
NVIDIA SM = (Intel slice 안 sub-slice 의 EU 묶음) 으로 대략 끼워 맞을 수 있다. 정확한 ratio 는 chip 별로 다르지만, occupancy 를 결정하는 단위가 EU 의 active hardware thread 수라는 게 가장 중요하다.
// sub-group 사용 — Intel GPU 의 native warp 폭 활용
q.submit([&](handler& cgh){
cgh.parallel_for(
nd_range<1>{global, local},
[=](nd_item<1> it)
[[intel::reqd_sub_group_size(16)]] {
auto sg = it.get_sub_group();
int id = sg.get_local_id();
// sub-group 내 reduce — CUDA shfl_xor 동치
float v = data[it.get_global_id(0)];
float sum = reduce_over_group(sg, v, plus<>{});
if (id == 0) atomic_ref(out[0])
.fetch_add(sum);
});
});
reqd_sub_group_size(16) 이 Xe-HPC 의 native sub-group 폭. 이 attribute 가 빠지면 컴파일러가 8 / 16 / 32 사이에서 자동 선택하는데, perf 코드에서는 항상 명시한다.
SYCL 은 Khronos 표준이고, DPC++ (Data Parallel C++) 는 Intel 이 그 표준을 구현한 컴파일러 + 확장이다. oneAPI 는 DPC++ 위에 쌓인 vendor 라이브러리 묶음 — 정확히 CUDA 위의 cuBLAS / cuDNN / NCCL 자리다.
SYCL 은 vendor 중립의 spec이고, DPC++ 는 그 spec 의 Intel 구현이다. NVIDIA GPU 위에서도 SYCL 코드를 돌릴 수 있는데, 그 경로는 보통 — DPC++ → SPIR-V → CUDA backend (Codeplay / oneAPI plugin) — 으로 깔린다. 즉 최종적으로는 PTX 가 나온다. AMD 도 비슷하게 ROCm 위에 plugin.
torch.xpu 와 IPEX2024 년 후반부터 PyTorch 본체에 XPU backend 가 합류했다. CUDA 의 torch.cuda 처럼 torch.xpu.is_available() 가 동작한다. 그 이전 길은 IPEX (Intel Extension for PyTorch) — 같은 작업을 plug-in 형태로 했다.
# PyTorch XPU backend (≥ 2.4)
import torch
assert torch.xpu.is_available()
device = "xpu:0"
x = torch.randn(8192, 8192, device=device)
y = torch.randn(8192, 8192, device=device)
z = x @ y # oneDNN GEMM 호출됨
torch.xpu.synchronize()
# IPEX 로 inference 가속 — fused op 자동 매칭
import intel_extension_for_pytorch as ipex
model = ipex.optimize(model, dtype=torch.float16)
out = model(x.to("xpu"))
강의에서 Patric 이 데모로 보여준 작업 순서.
device="xpu" 만 바꾸고 모델이 도는지 본다.torch.utils.cpp_extension 의 SYCL 변형이 등장 (DPC++ 기반).이 셋의 관계는 CUDA 쪽 torch.cuda + torch.compile + load_inline 의 관계와 정확히 평행한다. 강의의 한 메시지 — “CUDA 코드 쓰는 마음으로 SYCL 을 쓸 수 있다” 는 게 인지적 frame.
PyTorch 안에서 x.to("xpu") 를 부르면 — ATen op 의 XPU dispatch 키가 호출되고, oneDNN 또는 oneMKL 의 vendor 커널이 실행된다. 즉 같은 torch.matmul 한 줄이 device 키에 따라 cuBLAS / oneMKL / Apple MPS / ROCm 으로 갈린다. Frontend 는 같고, dispatch 가 갈라진다.
강의의 큰 지렛대 중 하나는 — Patric 이 청중을 “CUDA 알고 있다” 로 가정한다는 것. 그래서 곳곳에서 CUDA → SYCL 직역 을 던진다. 한 페이지로 정리해두면 코드 읽기가 즉시 빨라진다.
sycl::queue. 둘 다 비동기 실행 채널. queue 가 dependency 추적까지 한다는 점이 다르다.malloc_device + q.memcpy(...). 또는 malloc_shared 로 한 번에.cgh.parallel_for(range, [=](id i){ ... }). lambda body 가 device code.q.submit + parallel_for(nd_range<N>{global,local}, ...)local_accessor<float, 1> s({N}, cgh) — handler 안에서 선언, work-group 내 공유.it.barrier() — work-group barrier.shift_group_left/right, permute_group_by_xor — sub-group level shuffle.q.wait() 또는 q.wait_and_throw().sycl::ext::oneapi::experimental::command_graph — 강의의 Q&A 에서 명시적으로 언급됨.sycl::event. q.submit(...) 의 반환값. dependency wiring 에 직접 쓴다.이 mapping 위에서 한 줄로 더 짚어야 하는 점 — SYCL 은 dependency 를 queue 가 자동 추적한다. CUDA 에서 stream 위 같은 work 는 자동 직렬화되지만, 다른 stream 으로 분리하면 이벤트로 동기화를 사용자가 한다. SYCL 은 같은 buffer/USM 에 대한 read/write 의존을 보고 알아서 묶는다 — “잘못된 동기화로 race 가 나는 일이 줄어든다” 가 강의의 작은 메시지.
강의 후반부의 본론 — Intel GPU 위에서 LLM inference 의 hot path 를 SYCL 로 다시 짠 사례. fused RMSNorm + matmul, flash attention v2, MHA fusion. CUDA 쪽의 같은 개념과 같은 자리에 박힌다.
fused MHA 의 구조는 NVIDIA flash attention 과 정확히 같다.
코드의 outer 루프와 inner 루프가 CUDA 코드와 거의 1:1. 다른 점은 단어와 dispatch 만 — load_2d_block(Intel ESIMD intrinsic)와 joint_matrix_mad(XMX) 가 CUDA 의 cp.async 와 mma.sync 자리.
LLaMA-7B inference, batch 1, seq 2048. baseline (oneDNN unfused) 대비 SYCL fused MHA 가 토큰당 latency 1/2.8. HBM traffic 이 거의 한 자리수로 감소. 같은 칩 위 cuBLAS 같은 dense math 라이브러리만 부르는 코드 대비 큰 polynomial 차이가 아니라, kernel fusion 자체로 따 내는 것이 절반 이상이라는 점이 중요.
SYCL 의 가장 큰 마케팅은 vendor portability다. Intel · NVIDIA · AMD · 일부 NPU. 그런데 “portable 하지만 portable performance 는 보장 안된다” 는 갭이 늘 따라온다. 강의에서 Patric 이 이 갭을 명시적으로 인정한다.
joint_matrix(XMX), esimd, NVIDIA 의 mma intrinsic 같은 것.“portability” 는 코드가 빌드되고 정답을 낸다는 의미. 가장 빠른 코드를 원한다면 vendor specific intrinsic(XMX, mma, mfma)을 직접 부르게 된다. SYCL 의 가치는 — fallback 코드가 한 벌, hot path 만 vendor 분기 — 라는 작은 표면적이다. “같은 파일 안에서 #ifdef 으로 vendor 갈라낸다” 가 실전 패턴.
강의에서 6개월 뒤 돌아왔을 때 가장 빨리 복원해야 하는 사실들과 — 직접 손에 박아야 하는 코드 자료들.
queue, handler, USM/accessor, parallel_for + nd_range. 이 넷이 vector add 부터 GEMM 까지 다 짠다.reqd_sub_group_size attribute 로 명시 권장.device="xpu". torch.xpu.synchronize(). IPEX optimize 로 fused op 자동 적용.icpx -fsycl vector_add.cpp -o vec. 같은 코드를 NVIDIA backend 로 빌드해서 두 device 위에서 같은 결과를 얻는다.__shared__ → local_accessor, __syncthreads → barrier mapping 을 직접 손에 박는다.torch.xpu 로 LLaMA-2 7B inference 를 돌린다. fp16 / bf16 으로 토큰당 latency 측정.ipex.optimize 한 줄. fused op 가 켜졌을 때의 차이를 본다.local_accessor<float> 로 work-group 내 reduction 을 직접 짠다. reduce_over_group 빌트인과 손으로 짠 버전의 perf 비교.permute_group_by_xor.SYCL 자체는 GPU Mode 시리즈에서 외로운 강의지만, 그 안의 개념들 — fused kernel, flash attention, vendor portability — 은 시리즈 전체와 연결된다.
학습 노트로 정리하면서 비워둔 자리들 — 강의 안에서 부분적으로만 등장한 주제, 또는 후속 검증이 필요한 주제.
이 노트의 측정 수치(fused MHA 2.8×, HBM traffic 0.23×)는 강의에서 Patric 이 보여준 슬라이드 값을 재구성한 예시다. 자기 환경(Arc / Data Center GPU Max / Habana)에서 직접 baseline 을 떠봐야 vendor 간 비교가 의미를 가진다.