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 안정성 확보에 필요한 실전 내용을 다룹니다.

전제 조건: GPU 서브시스템 (DRM/KMS)CPU 스케줄러 문서를 먼저 읽으세요. drm_sched는 GPU 명령 큐 관리와 동기화 프리미티브(fence)를 다루므로, DRM 기본 구조와 커널 동시성 개념이 필수입니다.
일상 비유: drm_sched는 식당 주문 시스템과 비슷합니다. 손님(프로세스)이 주문(GPU job)을 넣으면, 웨이터(drm_sched)가 우선순위에 따라 주방(GPU hardware)에 전달합니다. 주방이 하나의 음식을 너무 오래 만들면(GPU hang), 웨이터는 그 주문을 취소하고 주방을 리셋합니다(TDR).

핵심 요약

  • 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

단계별 이해

  1. drm_gpu_scheduler 구조 파악
    스케줄러, entity, job의 계층 관계를 이해합니다.
  2. Job 라이프사이클 추적
    push_job → scheduled → run → fence signal 전체 흐름을 따라갑니다.
  3. dma_fence 동기화 이해
    fence_ops와 signaling 메커니즘을 파악합니다.
  4. 우선순위 큐 동작 확인
    여러 entity가 어떻게 라운드 로빈으로 스케줄되는지 이해합니다.
  5. TDR 흐름 분석
    timedout_job 콜백과 GPU 리셋 절차를 학습합니다.
  6. 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, semaphoredma_fence (GPU 인터럽트 기반)
타임슬라이싱jiffies 기반 선점Job 단위 (Job 중단은 드문 편)
우선순위nice값, cgroupDRM_SCHED_PRIORITY_* 4단계
장애 복구프로세스 종료GPU 리셋 (TDR)
drm_sched 위치: drivers/gpu/drm/scheduler/에 위치하며, GPU 드라이버(amdgpu, i915, nouveau, panfrost, lima 등)에서 공통으로 사용합니다. 헤더는 include/drm/gpu_scheduler.h에 있습니다.

drm_sched 아키텍처

유저 프로세스 (Mesa / ROCm / CUDA / OpenCL) Process A (3D게임) Process B (AI추론) Process C (비디오) Process D (컴퓨팅) drm_sched_entity (프로세스별 Job 큐) entity A [JOB2→JOB1] PRIORITY_HIGH entity B [JOB3] PRIORITY_NORMAL entity C [JOB5→JOB4] PRIORITY_NORMAL entity D [JOB6] PRIORITY_LOW drm_gpu_scheduler (GFX / COMPUTE / SDMA / UVD 엔진별) run_queue[HIGH] run_queue[NORMAL] run_queue[LOW] timeout watchdog 드라이버 백엔드 (drm_sched_backend_ops) run_job() → GPU 링 버퍼에 명령 기록 · timedout_job() → GPU 리셋 · free_job() → 메모리 해제 GPU Hardware (GFX엔진 / Compute엔진 / SDMA) 링 버퍼 읽기 · 커맨드 실행 · 완료 인터럽트 발생

핵심 구조체

#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에서 실행 완료되기까지의 전체 흐름을 설명합니다.

INIT job 생성 DEPENDENCY fence 대기 SCHEDULED run_queue 대기 RUNNING GPU 실행 중 DONE fence signal TIMEOUT hang 감지 RESET GPU 리셋 push_job fence done run_job() IRQ signal timeout TDR drm_sched_job_init() drm_sched_job_arm() drm_sched_push_job() drm_sched_backend_ops free_job() 재제출 또는 오류 처리

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 구현 비교

Intel i915 + GuC drm_sched + GuC 마이크로컨트롤러 GuC가 실제 스케줄링 결정 수행 GuC Submission Path drm_sched_job → GuC H2G → GuC 큐잉 GuC G2H → 완료 fence signal 선점: GuC 하드웨어 컨텍스트 전환 마이크로초 단위 컨텍스트 스위치 TDR: GuC Watchdog + i915 GPU reset engine reset 또는 full GPU reset 소스: drivers/gpu/drm/i915/gt/uc/ intel_guc_submission.c AMD amdgpu drm_sched 직접 사용 커널이 직접 스케줄링 결정 Ring Buffer Submission IB (Indirect Buffer) 링에 기록 CP (Command Processor) 하드웨어 처리 선점: amdgpu ring stop/start VMID 전환, 파이프라인 플러시 TDR: drm_sched timedout_job → amdgpu_device_gpu_recover() 소스: drivers/gpu/drm/amd/amdgpu/ amdgpu_job.c, amdgpu_ring.c

하드웨어 큐 관리 (링 버퍼 / 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.cdrm_sched 핵심 — kthread, run_queue, TDR
drivers/gpu/drm/scheduler/sched_entity.centity 관리 — push_job, pop_job
drivers/gpu/drm/scheduler/sched_fence.cdrm_sched_fence — Job 완료 fence 생성
include/drm/gpu_scheduler.h공개 API — 모든 구조체와 함수 선언
drivers/dma-buf/dma-fence.cdma_fence 기본 구현
drivers/dma-buf/dma-fence-chain.cfence chain — 순서 보장
drivers/gpu/drm/amd/amdgpu/amdgpu_job.camdgpu drm_sched 통합
drivers/gpu/drm/amd/amdgpu/amdgpu_ring.camdgpu 링 버퍼 관리
drivers/gpu/drm/i915/gt/uc/intel_guc_submission.ci915 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 추적
다음 학습 경로: