Skip to content

플러그인 작성을 상징하는 이미지

앞선 글에서 "새 카드 지원은 엔진 포크가 아니라 kernels/에 .so 하나"라는 문장을 몇 번 봤다. docs/PLUGIN.md는 그 문장이 어떻게 가능한지를 정의한 문서다. 전체를 다 옮길 수는 없고, 플러그인을 시작할 때 필요한 계약과 구조를 개관한다.

플러그인은 radiance에 링크되지 않는다 ​

세 종류의 플러그인이 엔진을 확장한다.

  • 커널 라이브러리 — 어떤 하드웨어(카드, 카드 families, CPU)의 op 구현
  • 아키텍처 — 모델의 가중치·버퍼·op 발행 순서를 선언
  • 양자화기 — rad-convert가 체크포인트 가중치를 인코딩 평면으로 바꾸는 방법

세 종류 전부 radiance의 어떤 코드에도 링크되지 않은 공유 오브젝트이고, 설치된 abi/ 헤더만으로 컴파일된다. 그래서 radiance를 빌드하지 않은 서드파티도 플러그인을 배포할 수 있다. 저장소 안의 기본 플러그인들(libr4d, libref, libavx, libquant, 모든 arch)도 rad_add_plugin으로 똑같이 빌드되고, 자기 이름을 기준으로가 아니라 선언한 내용으로 순위가 매겨진다. ctest -L oot은 저장소 밖에서 설치본에 대해 다시 빌드해 같은 결과가 나오는지 검사한다.

ABI 계약: 버전과 레이아웃 지문 ​

플러그인의 첫 exports는 계약 두 개다.

c
uint32_t rad_plugin_abi_version(void) { return RAD_ABI_VERSION; }
RAD_PLUGIN_ABI_LAYOUT_EXPORT    /* uint64_t rad_plugin_abi_layout(void) */

const RadPluginInfo* rad_plugin_info(void) {
    static const RadPluginInfo i = {
        RAD_PLUGIN_KERNEL, "mykernels", "0.1", "a GEMM or two", "gfx1100",
    };
    return &i;
}
  • ABI 버전이 엔진과 다르면 플러그인은 로드 거부된다 — 이름과 두 숫자가 인쇄된다. 매크로를 반환하고 헤더 구조가 바뀌는 릴리스에서 다시 빌드한다.
  • 레이아웃 지문은 헤더들을 자기 쪽에서 컴파일했을 때의 struct 레이아웃(크기·오프셋) 해시다. 같은 버전 숫자라도 다른 트리의 헤더로 다른 레이아웃을 만들어 붙인 플러그인은 이 지문으로 걸러진다. 세 종류 플러그인 전부 두 export를 내보낸다.

build_target("gfx1100", "host" 등)는 인쇄만 하고 신뢰하지 않는다 — 엔진이 라이브러리가 실제로 든 코드 오브젝트를 검사한다. 그 외 선택 export로 rad_plugin_open(머신이 안 맞으면 RAD_E_UNSUPPORTED로 조용히 빠짐), rad_kernel_concurrent(독립 launch 겹침 허용), rad_kernel_collective(링크 트래픽 분리), rad_plugin_reference를 선언할 수 있다.

커널이 자신을 선언한다: 제약과 우선순위 ​

각 커널은 자기가 무엇에 대해 옳은지를 선언한다.

c
static const RadConstraint c_gemm[] = {
    RAD_CLE("M", 64),               /* M <= 64             */
    RAD_CDIV("K", 128),             /* K is divisible by 128 */
    RAD_CIN("dtype", "fp8a8 w4a8")  /* dtype is in that set */
};

static const RadKernelInfo k[] = {{
    .name = "mygemm_m64", .op = "gemm_nt_q", .family = "gemm",
    .dtypes   = "fp8a8, w4a8",
    .domain   = RAD_DOMAIN_DEVICE,
    .priority = 50,
    .constraints = c_gemm, .n_constraints = 3,
    .launch = my_launch,
}};

엔진은 이 선언을 믿는다 — M <= 2048이라 선언하고 64까지만 옳으면 거짓말이다. 문제는 동시성에서 M이 커질 때만 드러나니, 문서가 "C=1이 아니라 동시성 사다리를 전부 시험하라"고 강조하는 이유다. device.로 시작하는 키는 op 인자가 아니라 launch 시 측정한 기계 사실과 비교된다(device.p2p == 1을 요구하는 커널은 P2P가 안 되는 짝에서 선택되지 않고, 그 자리를 _host 형제가 받는다 — 9편의 P2P 이야기가 이 메커니즘이다). 연산을 새로 정의하거나 한 스텝에 여러 구현을 섞는 방법, 작업 확인 절차까지 PLUGIN.md에 이어진다.

참고 구현 선언과 libref의 의미 ​

rad_plugin_reference를 nonzero로 선언한 라이브러리는 참조 구현이 된다: 이름 없는 라이브러리 전부보다 순위가 낮고, --debug-accept-reference-kernels 없이는 서빙에 거부되며, rad-kbench가 다른 커널을 검사하는 기준이 된다. libref가 자기 자신을 이렇게 선언한다. 그래서 "libref로 해석된 op 목록"은 곧 새 라이브러리가 해야 할 남은 일의 목록이다 — 2편에서 본 설계가 플러그인 계약으로 구현된 지점이다.

아키텍처 플러그인 ​

아키텍처 플러그인은 RadArchDef로 모델 families의 그래프를 선언한다 — 가중치 계획, 버퍼, op 발행 순서, 그리고 프리픽스 캐시·초안 깊이·activation arena 재선언 같은 capabilities. gemma4, 512-expert 하이브리드를 선언한 qwen4exp_fp8 같은 것이 실제 사례다. 모델에 맞는 op vocabulary(OPS.md)를 따라 선언하고, --debug-graph가 그 선언에서 만들어진 그래프을 보여준다(6편).

양자화기 플러그인 ​

양자화기 플러그인은 레시피의 PATTERN QUANTISER OPTIONS에서 이름으로 불리는 함수다. 체크포인트 텐서를 받아 인코딩의 평면(부호, 스케일, 영점, 회전)을 쓰고, 필요하면 보정 데이터를 받는다(10편의 GPTQ/$CALIB). 새로운 양자화 포맷을 붙인다는 것은 곧 레시피 문법에 새 단어 하나를 더하는 일이다.

빌드, 설치, 검색 경로 ​

플러그인 빌드의 출발점은 설치된 radiance의 헤더다: <prefix>/include/radiance/(ABI 헤더)와 lib/cmake/radiance/(find_package(radiance)). 산출된 .so는 $RADIANCE_HOME의 kernels/, architectures/, quantizers/ 중 알맞은 곳에 두고, RADIANCE_HOME=내_경로:/opt/radiance/share/radiance처럼 설치본 앞에 검색 경로를 세운다. rad-info --plugins가 경로에 무엇이 있는지, --debug-selection이 무엇 선택됐는지 보여준다. 카드 자체를 새로 지원하는 순서 — 대상 카드를 RAD_GPU_TARGETS에 추가해 엔진을 빌드하고, PLUGIN.md §10의 walkthrough대로 커널 라이브러리를 채우고, rad-kbench로 검증한다 — 은 이 문서가 한 장(§10)으로 안내한다.

엔진을 포크하지 않는 확장, 선언에 의한 선택, 지문과 버전으로 지키는 경계. radiance가 AMD 생태계 바깥 사람도 플러그인을 받을 수 있게 설계된 방식이다.

오픈소스를 한국어로 읽고 배웁니다.