drm_sched (GPU Job 스케줄러)
drm_sched를 Linux DRM 계층의 공통 GPU 작업 스케줄러로 심층 분석합니다. 다중 프로세스 job 큐 공정성, entity 기반 우선순위 제어, dma_fence 의존성 해석과 동기화, submit backlog와 워커 스레드 운영, timeout detection 및 GPU reset 복구 경로, amdgpu/i915 등 드라이버별 통합 패턴, 긴 작업과 짧은 작업 혼재 시 tail latency 관리, tracepoint/debugfs를 활용한 스케줄링 병목 분석까지 GPU 안정성 확보에 필요한 실전 내용을 다룹니다.
핵심 요약
- drm_gpu_scheduler — 하나의 GPU 엔진(링 버퍼)을 관리하는 스케줄러 인스턴스
- drm_sched_entity — 클라이언트별 Job 큐. 하나의 DRM 컨텍스트 = 하나의 entity
- drm_sched_job — GPU에 제출할 작업 하나. 의존 fence 목록 보유
- dma_fence — Job 완료 신호. CPU/GPU 간, GPU 엔진 간 동기화에 사용
- Priority — KERNEL > HIGH > NORMAL > LOW 4단계 우선순위
- TDR (Timeout Detection Recovery) — Job 타임아웃 시 GPU 리셋 후 복구
- i915 GuC — Intel GPU 마이크로컨트롤러가 스케줄링을 담당하는 방식
- ring buffer / IB — GPU 명령 버퍼 구조 — 링 버퍼와 Indirect Buffer
단계별 이해
- drm_gpu_scheduler 구조 파악
스케줄러, entity, job의 계층 관계를 이해합니다. - Job 라이프사이클 추적
push_job → scheduled → run → fence signal 전체 흐름을 따라갑니다. - dma_fence 동기화 이해
fence_ops와 signaling 메커니즘을 파악합니다. - 우선순위 큐 동작 확인
여러 entity가 어떻게 라운드 로빈으로 스케줄되는지 이해합니다. - TDR 흐름 분석
timedout_job 콜백과 GPU 리셋 절차를 학습합니다. - ftrace로 Job 추적
drm_sched 이벤트를 ftrace/bpftrace로 관찰합니다.
drm_sched 개요
GPU는 CPU와 달리 수천 개의 스레드를 병렬로 처리하는 하드웨어입니다. 여러 프로세스가 동시에 GPU를 사용하려 할 때, 각 프로세스의 명령 버퍼를 공정하고 효율적으로 GPU 하드웨어에 제출하는 소프트웨어가 필요합니다. 이것이 drm_sched의 역할입니다.
| 특성 | CPU 스케줄러 | drm_sched (GPU 스케줄러) |
|---|---|---|
| 스케줄 단위 | task_struct (프로세스/스레드) | drm_sched_job (GPU 커맨드 버퍼) |
| 컨텍스트 전환 | 레지스터 save/restore | 링 버퍼 포인터 전환 |
| 동기화 | spinlock, mutex, semaphore | dma_fence (GPU 인터럽트 기반) |
| 타임슬라이싱 | jiffies 기반 선점 | Job 단위 (Job 중단은 드문 편) |
| 우선순위 | nice값, cgroup | DRM_SCHED_PRIORITY_* 4단계 |
| 장애 복구 | 프로세스 종료 | GPU 리셋 (TDR) |
drivers/gpu/drm/scheduler/에 위치하며,
GPU 드라이버(amdgpu, i915, nouveau, panfrost, lima 등)에서 공통으로 사용합니다.
헤더는 include/drm/gpu_scheduler.h에 있습니다.
drm_sched 아키텍처
핵심 구조체
#include <drm/gpu_scheduler.h>
/* 1. drm_gpu_scheduler — 하나의 GPU 엔진 관리 */
struct drm_gpu_scheduler {
const struct drm_sched_backend_ops *ops; /* 드라이버 콜백 */
struct drm_sched_rq sched_rq[DRM_SCHED_PRIORITY_COUNT]; /* 우선순위별 큐 */
wait_queue_head_t wake_up_worker; /* 스케줄러 워커 웨이크업 */
struct task_struct *thread; /* kthread */
struct list_head pending_list; /* 실행 중인 Job 목록 */
long timeout; /* Job 타임아웃 (jiffies) */
atomic_t hw_rq_count; /* 실행 중 Job 수 */
const char *name; /* 디버그용 이름 */
};
/* 2. drm_sched_entity — 클라이언트별 Job 큐 */
struct drm_sched_entity {
struct spsc_queue job_queue; /* 단일 생산자-소비자 큐 */
struct drm_gpu_scheduler **sched_list; /* 사용 가능한 스케줄러 목록 */
unsigned int num_sched_list; /* 스케줄러 수 */
enum drm_sched_priority priority; /* 우선순위 */
struct dma_fence *dependency; /* 현재 대기 중인 의존 fence */
};
/* 3. drm_sched_job — GPU에 제출할 작업 */
struct drm_sched_job {
struct spsc_node queue_node; /* entity 큐의 노드 */
struct drm_sched_entity *entity; /* 소속 entity */
struct dma_fence s_fence; /* 완료 fence */
struct dma_fence *dependency; /* 선행 Job fence */
uint64_t id; /* Job 고유 ID */
struct list_head list; /* pending_list 노드 */
};
Job 라이프사이클
drm_sched_job이 생성되어 GPU에서 실행 완료되기까지의 전체 흐름을 설명합니다.
Job API 사용 예시
#include <drm/gpu_scheduler.h>
/* GPU 드라이버에서 Job 생성과 제출 흐름 */
static int my_gpu_submit_job(struct my_gpu_ctx *ctx,
struct my_gpu_cmd_buf *cmdbuf)
{
struct my_gpu_job *job;
struct dma_fence *fence;
int ret;
job = kzalloc(sizeof(*job), GFP_KERNEL);
/* 1. drm_sched_job 초기화 (entity와 연결) */
ret = drm_sched_job_init(&job->base, ctx->entity, 1, ctx);
if (ret) goto err;
/* 2. 의존 fence 추가 (이전 Job 완료 후 실행) */
ret = drm_sched_job_add_dependency(&job->base, dep_fence);
/* 3. Job 준비 (fence 서명 준비) */
fence = drm_sched_job_arm(&job->base);
/* 4. Job을 entity 큐에 추가 */
drm_sched_entity_push_job(&job->base);
/* fence를 유저스페이스에 반환 (syncobj 또는 out_fence) */
drm_syncobj_replace_fence(syncobj, fence);
dma_fence_put(fence);
return 0;
err:
kfree(job);
return ret;
}
/* drm_sched_backend_ops — GPU 드라이버가 구현 */
static const struct drm_sched_backend_ops my_sched_ops = {
.run_job = my_run_job, /* GPU에 커맨드 버퍼 제출 */
.timedout_job = my_timedout_job, /* 타임아웃 → GPU 리셋 */
.free_job = my_free_job, /* Job 완료 후 메모리 해제 */
};
dma_fence 기반 동기화
dma_fence는 GPU 작업 완료를 나타내는 커널 동기화 프리미티브입니다.
CPU가 GPU 완료를 기다리거나, GPU 엔진 간 순서를 보장하거나,
유저스페이스에 완료를 통보하는 모든 경우에 dma_fence가 사용됩니다.
fence_ops 구현
#include <linux/dma-fence.h>
/* GPU 드라이버별 fence 오퍼레이션 */
static const struct dma_fence_ops my_fence_ops = {
.get_driver_name = my_fence_get_driver_name,
.get_timeline_name = my_fence_get_timeline_name,
.enable_signaling = my_fence_enable_signaling, /* IRQ 활성화 */
.release = my_fence_release,
};
/* GPU 완료 인터럽트 핸들러에서 fence 시그널 */
static irqreturn_t my_gpu_irq_handler(int irq, void *data)
{
struct my_gpu *gpu = data;
struct my_job *job;
job = my_get_completed_job(gpu);
if (job) {
/* fence 시그널 → 대기 중인 CPU 스레드 깨움 */
dma_fence_signal(&job->fence);
/* drm_sched에 Job 완료 알림 */
drm_sched_job_cleanup(&job->base);
}
return IRQ_HANDLED;
}
/* CPU에서 GPU 완료 대기 */
int wait_for_gpu(struct dma_fence *fence, int64_t timeout_ns)
{
return dma_fence_wait_timeout(fence, true,
nsecs_to_jiffies(timeout_ns));
}
fence_chain — 순서 보장
/* dma_fence_chain: 여러 fence를 순서대로 연결 */
struct dma_fence_chain *chain;
chain = dma_fence_chain_alloc();
dma_fence_chain_init(chain, prev_fence, cur_fence, seqno);
/* 이후 cur_fence 대신 chain fence를 syncobj에 저장 */
/* → 이전 fence가 완료돼야 cur_fence도 완료로 처리 */
Priority Run-Queue
drm_sched는 4개의 우선순위별 run-queue를 유지하고, 스케줄러 kthread가 높은 우선순위 큐부터 라운드 로빈으로 entity를 선택합니다.
/* 우선순위 레벨 (include/drm/gpu_scheduler.h) */
enum drm_sched_priority {
DRM_SCHED_PRIORITY_MIN = 0, /* 가장 낮음 */
DRM_SCHED_PRIORITY_LOW = 0,
DRM_SCHED_PRIORITY_NORMAL = 1, /* 기본값 */
DRM_SCHED_PRIORITY_HIGH = 2, /* 실시간 렌더링 */
DRM_SCHED_PRIORITY_KERNEL = 3, /* 커널 작업 (리셋 등) */
DRM_SCHED_PRIORITY_COUNT,
};
/* entity 우선순위 변경 */
drm_sched_entity_set_priority(entity, DRM_SCHED_PRIORITY_HIGH);
/* 스케줄러 main loop (sched_thread 함수 내부 핵심 로직) */
while (!kthread_should_stop()) {
/* 높은 우선순위 큐부터 순서대로 entity 선택 */
entity = drm_sched_select_entity(sched);
if (!entity) {
wait_event_interruptible(sched->wake_up_worker, ...);
continue;
}
/* entity에서 Job 꺼내 실행 */
sched_job = drm_sched_entity_pop_job(entity);
fence = sched->ops->run_job(sched_job); /* GPU에 제출 */
drm_sched_job_begin(sched_job);
}
선점 (Preemption)
GPU 선점은 현재 실행 중인 Job을 중단하고 더 높은 우선순위 Job을 먼저 실행하는 기능입니다. drm_sched는 소프트웨어 수준의 선점(Job 경계에서만)과 하드웨어 선점(컨텍스트 저장/복원)을 지원합니다.
/* drm_sched_stop() — 하드웨어 이상 또는 선점 시 스케줄러 일시 중단 */
void drm_sched_stop(struct drm_gpu_scheduler *sched,
struct drm_sched_job *bad)
{
/* kthread 중단 */
kthread_park(sched->thread);
/* pending_list에서 완료되지 않은 Job 제거 */
list_for_each_entry_safe(s_job, tmp, &sched->pending_list, list) {
if (s_job == bad) break;
drm_sched_fence_scheduled(s_job->s_fence, NULL);
}
}
/* drm_sched_start() — 복구 후 스케줄러 재시작 */
void drm_sched_start(struct drm_gpu_scheduler *sched, bool full_recovery)
{
/* GPU 리셋 후 남은 Job 재제출 */
if (full_recovery) {
list_for_each_entry(s_job, &sched->pending_list, list)
drm_sched_fence_scheduled(s_job->s_fence, NULL);
}
kthread_unpark(sched->thread);
}
TDR (Timeout Detection Recovery)
GPU hang(행)은 GPU가 명령 처리를 멈추는 심각한 상황입니다.
drm_sched는 delayed work를 통해 Job 완료 타임아웃을 감지하고,
드라이버의 timedout_job 콜백을 호출하여 GPU를 리셋합니다.
TDR 흐름
/* drm_sched가 타임아웃 감지 시 호출하는 delayed work */
static void drm_sched_job_timedout(struct work_struct *work)
{
struct drm_gpu_scheduler *sched;
struct drm_sched_job *job;
sched = container_of(work, struct drm_gpu_scheduler,
work_tdr.work);
/* pending_list의 첫 번째 Job이 타임아웃 대상 */
job = list_first_entry_or_null(&sched->pending_list,
struct drm_sched_job, list);
if (job) {
/* 드라이버 timedout_job 콜백 호출 */
drm_sched_stop(sched, job);
enum drm_gpu_sched_stat stat =
sched->ops->timedout_job(job);
if (stat == DRM_GPU_SCHED_STAT_ENODEV) {
/* 하드웨어 불량 — 스케줄러 영구 중단 */
return;
}
drm_sched_start(sched, stat != DRM_GPU_SCHED_STAT_NOMINAL);
}
}
/* 드라이버 timedout_job 구현 예시 (amdgpu) */
static enum drm_gpu_sched_stat
amdgpu_job_timedout(struct drm_sched_job *job)
{
struct amdgpu_job *amdgpu_job = to_amdgpu_job(job);
/* 1. GPU hang 덤프 수집 */
amdgpu_device_gpu_recover(adev, amdgpu_job, &reset_context);
/* 2. GPU 풀 리셋 */
amdgpu_asic_reset(adev);
/* 3. 복구 완료 반환 */
return DRM_GPU_SCHED_STAT_NOMINAL;
}
i915 GuC vs amdgpu drm_sched 구현 비교
하드웨어 큐 관리 (링 버퍼 / IB)
GPU 드라이버는 drm_sched의 run_job 콜백에서 커맨드 버퍼를 GPU 링 버퍼에 기록합니다.
링 버퍼는 고정 크기의 원형 버퍼로, GPU의 Command Processor(CP)가 읽어서 실행합니다.
링 버퍼와 IB 구조
/* amdgpu 링 버퍼 커맨드 제출 예시 */
static struct dma_fence *amdgpu_job_run(struct drm_sched_job *sched_job)
{
struct amdgpu_job *job = to_amdgpu_job(sched_job);
struct amdgpu_ring *ring = job->ring;
/* 링 버퍼에 IB(Indirect Buffer) 실행 패킷 기록 */
amdgpu_ring_lock(ring, job->num_dw);
/* EXECUTE_INDIRECT 패킷: 실제 커맨드 버퍼 포인터 */
amdgpu_ring_write(ring, PACKET3(PACKET3_INDIRECT_BUFFER, 2));
amdgpu_ring_write(ring, lower_32_bits(job->ib.gpu_addr));
amdgpu_ring_write(ring, upper_32_bits(job->ib.gpu_addr));
amdgpu_ring_write(ring, job->ib.length_dw);
/* EOP(End-of-Pipe) 이벤트 기록 — 완료 시 인터럽트 발생 */
amdgpu_ring_write_eop_fence(ring, job->sync_seq);
amdgpu_ring_commit(ring); /* 링 버퍼 wptr 업데이트 */
return &job->base.s_fence->finished;
}
SDMA 큐 (DMA 엔진)
/* SDMA (System DMA) 큐 — 메모리 복사 전용 엔진 */
/* DMA 작업은 GFX 링과 별도의 SDMA 스케줄러 인스턴스 사용 */
/* amdgpu는 엔진 유형별로 별도 drm_gpu_scheduler 인스턴스를 생성: */
/* - adev->gfx.gfx_ring[i]: 그래픽스 링 (3D + 컴퓨팅) */
/* - adev->sdma.instance[i].ring: DMA 엔진 링 */
/* - adev->vcn.inst[i].ring_dec: 비디오 디코더 링 */
/* 각각 독립적인 drm_sched_entity와 drm_gpu_scheduler 사용 */
진단 — ftrace와 bpftrace
ftrace drm_sched 이벤트
# drm_sched 추적 이벤트 목록
ls /sys/kernel/debug/tracing/events/drm_sched/
# 모든 drm_sched 이벤트 활성화
echo 1 > /sys/kernel/debug/tracing/events/drm_sched/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace_pipe
# 예시 출력:
# kwin_wayland-1234 [003] drm_sched_job: sched=amdgpu_gfx.0.0 job=1234
# kwin_wayland-1234 [003] drm_run_job: sched=amdgpu_gfx.0.0 job=1234
# irq/42-amdgpu-1 [002] drm_sched_process_job: fence=0xffff... seqno=5678
bpftrace GPU Job 추적
# Job 제출 지연시간 측정 (push → GPU 실행까지)
bpftrace -e '
kprobe:drm_sched_entity_push_job {
@ts[arg0] = nsecs;
}
kprobe:drm_sched_backend_ops__run_job {
$job = (struct drm_sched_job *)arg0;
if (@ts[$job]) {
@latency = hist(nsecs - @ts[$job]);
delete(@ts[$job]);
}
}'
# GPU hang (timedout_job) 감지
bpftrace -e '
kprobe:drm_sched_job_timedout {
$sched = (struct drm_gpu_scheduler *)arg0;
printf("[TDR] GPU hang on scheduler: %s\n",
str($sched->name));
}'
# dma_fence 신호 지연 분석
bpftrace -e '
kprobe:dma_fence_signal {
@ts[arg0] = nsecs;
}
kretprobe:dma_fence_signal {
printf("fence signal: %ldns\n", nsecs - @ts[arg0]);
delete(@ts[arg0]);
}'
debugfs 진단
# amdgpu GPU 스케줄러 상태
cat /sys/kernel/debug/dri/0/amdgpu_sched_full
# GFX_SCHED_0.0: pending: 3, hw_rq: 1
# ENTITY: prio=NORMAL, jobs=2
# i915 GuC 스케줄러 상태
cat /sys/kernel/debug/dri/0/i915_guc_load_status
cat /sys/kernel/debug/dri/0/i915_scheduler_info
# dma_fence 추적 (CONFIG_DMA_FENCE_TRACE=y 필요)
echo 1 > /proc/sys/kernel/dma_fence_enable_signal_fences_debug
커널 소스 가이드
| 파일 / 디렉토리 | 설명 |
|---|---|
drivers/gpu/drm/scheduler/sched_main.c | drm_sched 핵심 — kthread, run_queue, TDR |
drivers/gpu/drm/scheduler/sched_entity.c | entity 관리 — push_job, pop_job |
drivers/gpu/drm/scheduler/sched_fence.c | drm_sched_fence — Job 완료 fence 생성 |
include/drm/gpu_scheduler.h | 공개 API — 모든 구조체와 함수 선언 |
drivers/dma-buf/dma-fence.c | dma_fence 기본 구현 |
drivers/dma-buf/dma-fence-chain.c | fence chain — 순서 보장 |
drivers/gpu/drm/amd/amdgpu/amdgpu_job.c | amdgpu drm_sched 통합 |
drivers/gpu/drm/amd/amdgpu/amdgpu_ring.c | amdgpu 링 버퍼 관리 |
drivers/gpu/drm/i915/gt/uc/intel_guc_submission.c | i915 GuC submission 드라이버 |
커널 설정
# DRM GPU 스케줄러 (GPU 드라이버 선택 시 자동)
CONFIG_DRM_SCHED=y # drm_sched 코어
CONFIG_DRM_AMDGPU=y # AMD GPU (drm_sched 주요 사용자)
CONFIG_DRM_I915=y # Intel GPU + GuC 스케줄러
CONFIG_DRM_NOUVEAU=y # Nouveau (NVIDIA 오픈소스)
CONFIG_DRM_PANFROST=y # ARM Mali GPU
# 디버그 옵션
CONFIG_DRM_SCHED_TRACEPOINTS=y # ftrace 이벤트 활성화
CONFIG_DMA_FENCE_TRACE=y # dma_fence 추적
관련 문서
- GPU 서브시스템 (DRM/KMS) — drm_sched가 속한 DRM 프레임워크 전체 구조
- NPU (Neural Processing Unit) — drm_sched를 사용하는 AI 가속기 드라이버
- CPU 스케줄러 — drm_sched와 비교되는 CPU 스케줄링
- DMA — dma_fence와 DMA 완료 인터럽트 연동
- HMM (이기종 메모리 관리) — GPU Job 실행 시 메모리 마이그레이션