drm_sched (GPU Job 스케줄러(Scheduler))

drm_sched를 Linux DRM 계층의 공통 GPU 작업 스케줄러로 심층 분석합니다. 다중 프로세스(Process) job 큐 공정성(Fairness), entity 기반 우선순위(Priority) 제어, dma_fence 의존성 해석과 동기화, submit backlog와 워커 스레드(Thread) 운영, timeout detection 및 GPU reset 복구 경로, amdgpu/i915 등 드라이버별 통합 패턴, 긴 작업과 짧은 작업 혼재 시 tail latency 관리, tracepoint/debugfs를 활용한 스케줄링 병목(Bottleneck) 분석까지 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 엔진(링 버퍼(Ring Buffer))을 관리하는 스케줄러 인스턴스
  • 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 명령 버퍼(Buffer) 구조 — 링 버퍼와 Indirect Buffer

단계별 이해

  1. drm_gpu_scheduler 구조 파악
    스케줄러, entity, job의 계층 관계를 이해합니다.
  2. Job 라이프사이클 추적
    push_job → scheduled → run → fence signal 전체 흐름을 따라갑니다.
  3. dma_fence 동기화 이해
    fence_ops와 signaling 메커니즘을 파악합니다.
  4. 우선순위 큐 동작 확인
    여러 entity가 어떻게 라운드 로빈(Round Robin)으로 스케줄되는지 이해합니다.
  5. TDR 흐름 분석
    timedout_job 콜백(Callback)과 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 커맨드 버퍼)
컨텍스트 전환레지스터(Register) save/restore링 버퍼 포인터 전환
동기화spinlock, mutex, semaphoredma_fence (GPU 인터럽트(Interrupt) 기반)
타임슬라이싱jiffies 기반 선점(Preemption)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) 링 버퍼 읽기 · 커맨드 실행 · 완료 인터럽트 발생

핵심 구조체(Struct)

#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 — 순서 보장(Ordering)

/* 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);
}

스케줄러 초기화와 해제

GPU 드라이버가 drm_sched를 사용하려면 엔진(링)별로 drm_gpu_scheduler를 초기화하고, 클라이언트(GEM context)별로 drm_sched_entity를 생성해야 합니다. 초기화 순서를 잘못 하면 Job 제출 시 커널 패닉(Kernel Panic)이 발생할 수 있습니다.

drm_sched_init() 호출

#include <drm/gpu_scheduler.h>

/* GPU 드라이버 probe 시 엔진별 스케줄러 초기화 */
static int my_gpu_sched_init(struct my_gpu *gpu)
{
    int ret;

    /* GFX 엔진 스케줄러 */
    ret = drm_sched_init(&gpu->sched_gfx,
                         &my_sched_ops,        /* 백엔드 콜백 */
                         NULL,                 /* submit_wq (NULL=내부 생성) */
                         1,                    /* hw_submission: 동시 실행 가능한 Job 수 */
                         0,                    /* hang_limit: 0=무제한 리셋 */
                         msecs_to_jiffies(10000),  /* timeout: 10초 */
                         NULL,                 /* timeout_wq */
                         NULL,                 /* score (부하 분산용) */
                         "my_gpu_gfx",         /* 디버그 이름 */
                         gpu->dev->dev);
    if (ret)
        return ret;

    /* DMA 엔진 스케줄러 (별도 인스턴스) */
    ret = drm_sched_init(&gpu->sched_dma,
                         &my_dma_sched_ops,
                         NULL, 2, 0,
                         msecs_to_jiffies(5000),
                         NULL, NULL,
                         "my_gpu_dma",
                         gpu->dev->dev);
    return ret;
}

/* 드라이버 제거 시 해제 — 반드시 entity보다 나중에 */
static void my_gpu_sched_fini(struct my_gpu *gpu)
{
    drm_sched_fini(&gpu->sched_dma);
    drm_sched_fini(&gpu->sched_gfx);
}

Entity 초기화와 해제

/* 클라이언트 GEM context 생성 시 entity 초기화 */
static int my_ctx_create(struct my_gpu *gpu,
                         struct my_ctx *ctx)
{
    struct drm_gpu_scheduler *scheds[] = {
        &gpu->sched_gfx,   /* 부하 분산 시 여러 스케줄러 지정 가능 */
    };

    return drm_sched_entity_init(&ctx->entity,
                                 DRM_SCHED_PRIORITY_NORMAL,
                                 scheds,
                                 ARRAY_SIZE(scheds),
                                 NULL);  /* guilty 플래그 */
}

/* GEM context 파괴 시 entity 정리 */
static void my_ctx_destroy(struct my_ctx *ctx)
{
    /* entity 큐에 남은 Job이 완료될 때까지 대기 후 해제 */
    drm_sched_entity_destroy(&ctx->entity);
}
hw_submission 파라미터 주의: hw_submission은 GPU 하드웨어가 동시에 처리할 수 있는 Job 수입니다. 이 값을 너무 크게 설정하면 타임아웃 감지가 늦어지고, 너무 작게 설정하면 GPU 파이프라인(Pipeline)이 비어 성능이 저하됩니다. amdgpu는 링당 보통 256을, panfrost는 1~2를 사용합니다.

Entity 마이그레이션과 부하 분산(Load Balancing)

동일한 유형의 GPU 엔진이 여러 개 있을 때(예: amdgpu의 compute ring 0~7), drm_sched는 entity를 가장 부하가 적은 스케줄러로 자동 마이그레이션할 수 있습니다. 이는 drm_sched_entity_init()에 복수의 스케줄러를 전달하여 활성화합니다.

/* 부하 분산: 여러 compute 엔진에 entity 분배 */
struct drm_gpu_scheduler *compute_scheds[8];
for (int i = 0; i < 8; i++)
    compute_scheds[i] = &adev->gfx.compute_sched[i];

/* entity 초기화 시 모든 compute 스케줄러를 후보로 전달 */
drm_sched_entity_init(&ctx->compute_entity,
                     DRM_SCHED_PRIORITY_NORMAL,
                     compute_scheds,
                     8,     /* num_sched_list */
                     NULL);

/* drm_sched_entity_pop_job() 내부에서 부하 분산 수행:
 *   1. entity->num_sched_list > 1이면
 *   2. 각 스케줄러의 score(atomic_t)를 비교
 *   3. 가장 score가 낮은 스케줄러로 entity 이동
 *   4. run_queue에서 entity를 제거하고 새 run_queue에 추가
 */
Entity 부하 분산 (Load Balancing) drm_sched_entity Pool (num_sched_list = 4) entity A (JOB) entity B (JOB) entity C (migrate) entity D (JOB) entity E score 비교 후 이동 compute_sched[0] score=12 (busy) compute_sched[1] score=7 (moderate) compute_sched[2] score=2 (idle) compute_sched[3] score=5 (moderate) GPU Compute Engines (CU 0~3)
score 산출 방식: drm_sched_entity_pop_job()에서 Job을 꺼낼 때마다 현재 스케줄러의 score(pending Job 수 기반)를 다른 후보 스케줄러와 비교합니다. 더 낮은 score의 스케줄러가 있으면 entity를 이동시켜, Job이 실제로 GPU에 제출될 때 가장 여유 있는 엔진이 선택됩니다.

선점 (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;
}

공정성과 기아(Starvation) 방지 (Fairness)

drm_sched는 여러 프로세스가 GPU를 공유할 때 특정 프로세스가 GPU를 독점하지 않도록 라운드 로빈 기반 공정성 메커니즘을 제공합니다. 같은 우선순위의 entity들은 순서대로 Job을 하나씩 꺼내어 GPU에 제출됩니다.

Run-Queue 라운드 로빈

/* drm_sched_rq 구조 (우선순위별 큐) */
struct drm_sched_rq {
    spinlock_t          lock;
    struct list_head    entities;     /* entity 리스트 */
    struct drm_sched_entity *current_entity; /* 마지막 선택된 entity */
};

/* entity 선택 로직 (sched_main.c) */
static struct drm_sched_entity *
drm_sched_rq_select_entity_rr(struct drm_sched_rq *rq)
{
    struct drm_sched_entity *entity;

    spin_lock(&rq->lock);
    /* current_entity 다음부터 탐색 → 라운드 로빈 보장 */
    entity = rq->current_entity ?
        list_next_entry(rq->current_entity, list) :
        list_first_entry(&rq->entities, ...);

    /* Job이 있는 entity를 찾을 때까지 순회 */
    list_for_each_entry_continue(entity, &rq->entities, list) {
        if (drm_sched_entity_is_ready(entity)) {
            rq->current_entity = entity;
            spin_unlock(&rq->lock);
            return entity;
        }
    }
    spin_unlock(&rq->lock);
    return NULL;
}
시나리오동작결과
Entity A: 100개 Job, Entity B: 1개 Job라운드 로빈으로 교대 선택B의 Job이 A의 100번째 Job보다 먼저 제출
HIGH 우선순위 entity 존재HIGH run_queue를 먼저 확인NORMAL entity는 HIGH가 비어야 실행
모든 entity의 Job이 fence 대기스케줄러 kthread sleepfence signal 시 wake_up_worker
entity 큐 오버플로우spsc_queue가 꽉 차면 push 차단유저 프로세스가 ioctl에서 대기
기아(Starvation) 주의: drm_sched는 KERNEL/HIGH 우선순위에 대한 rate-limiting이 없습니다. KERNEL 우선순위 entity가 계속 Job을 제출하면 NORMAL/LOW entity는 무기한 대기합니다. 이는 GPU 리셋 복구 Job이 항상 최우선 실행되도록 의도된 설계입니다. 일반 드라이버에서는 HIGH를 남용하지 않도록 주의하세요.

메모리 관리(Memory Management)와 Job 제출

GPU Job은 GPU가 접근할 메모리(커맨드 버퍼, 텍스처, 버텍스 버퍼 등)를 사용합니다. Job 실행 중에 해당 메모리가 evict되거나 해제되면 GPU hang이 발생하므로, drm_sched와 GEM/TTM 메모리 관리자는 긴밀하게 연동합니다.

BO Reservation과 Job 의존성

/* Job 제출 전 Buffer Object(BO) 예약 */
static int my_submit(struct my_ctx *ctx,
                     struct my_bo **bos, int num_bos)
{
    struct drm_exec exec;
    int ret;

    /* 1. 모든 BO에 대한 reservation lock 획득 (데드락 방지) */
    drm_exec_init(&exec, DRM_EXEC_INTERRUPTIBLE_WAIT, 0);
    drm_exec_until_all_locked(&exec) {
        for (int i = 0; i < num_bos; i++) {
            ret = drm_exec_prepare_obj(&exec,
                                        &bos[i]->base, 1);
            drm_exec_retry_on_contention(&exec);
            if (ret) goto err;
        }
    }

    /* 2. BO에 연결된 기존 fence를 Job 의존성으로 추가 */
    for (int i = 0; i < num_bos; i++) {
        ret = drm_sched_job_add_implicit_dependencies(
                &job->base, &bos[i]->base, true);
        if (ret) goto err;
    }

    /* 3. Job을 arm하고 BO에 새 fence 설정 */
    drm_sched_job_arm(&job->base);
    for (int i = 0; i < num_bos; i++)
        dma_resv_add_fence(bos[i]->base.resv,
                          &job->base.s_fence->finished,
                          DMA_RESV_USAGE_WRITE);

    /* 4. Job 제출 */
    drm_sched_entity_push_job(&job->base);

err:
    drm_exec_fini(&exec);
    return ret;
}
drm_exec 프레임워크: drm_exec은 여러 BO의 reservation lock을 데드락 없이 획득하는 헬퍼입니다. 내부적으로 ww_mutex(wound-wait)를 사용하며, contention 발생 시 자동으로 재시도합니다. 이전에 사용되던 ttm_eu_reserve_buffers()를 대체합니다.

Guilty Entity와 Ban 메커니즘

GPU hang을 반복적으로 유발하는 클라이언트(entity)를 자동으로 격리(Isolation)하는 메커니즘입니다. hang_limit을 초과한 entity는 "guilty"로 표시되어 이후 Job이 거부됩니다.

/* entity의 guilty 플래그 설정 */
struct drm_sched_entity {
    ...
    atomic_t   *guilty;      /* 공유 guilty 카운터 */
    ...
};

/* GPU hang 발생 시 guilty 처리 (sched_main.c 내부) */
if (sched->hang_limit &&
    entity->guilty &&
    atomic_inc_return(entity->guilty) > sched->hang_limit) {
    /*
     * 이 entity에서 제출한 Job이 hang_limit번 이상
     * GPU hang을 유발 → 이후 Job은 즉시 -ECANCELED
     */
    drm_sched_entity_kill(entity);
}

/* 유저스페이스에서 context guilty 상태 확인 (amdgpu 예시) */
/* amdgpu_cs_ioctl()에서 guilty entity의 Job은 -ECANCELED 반환 */
파라미터설명기본값사용 예시
hang_limitGPU hang 허용 횟수0 (무제한)amdgpu: amdgpu.lockup_timeout
guiltyentity별 hang 카운터NULL (비활성)GEM context당 1개 할당
timeoutJob 완료 대기 시간(Latency)드라이버 설정amdgpu: 10000ms, panfrost: 500ms

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 사용 */

drm_sched를 사용하는 드라이버 비교

drm_sched는 2016년 amdgpu에서 분리된 공통 프레임워크로, 현재 다양한 GPU 및 AI 가속기 드라이버에서 사용됩니다. 각 드라이버별 구현 세부사항을 비교합니다.

드라이버GPU 유형엔진 수hw_submissiontimeout (ms)특징
amdgpuAMD Radeon / InstinctGFX+Compute+SDMA+VCN25610000drm_sched의 주요 개발자, 가장 풍부한 구현
nouveauNVIDIA (오픈소스)GR+CE+NVDEC1615000firmware 기반 스케줄링과 혼합
panfrostARM Mali (Midgard/Bifrost)JS0+JS1+JS22500Job Slot 기반, 간단한 구조
limaARM Mali (Utgard)GP+PP1500매우 단순한 구현, PP 멀티코어
v3dBroadcom VideoCoreBIN+RENDER+TFU+CSD110000타일링 렌더러, bin/render 분리
etnavivVivante (i.MX)GPU 코어별22000임베디드 GPU, 다중 코어 지원
xeIntel (차세대)RCS+BCS+CCS+VCS645000i915 후속, GuC 기반
ivpuIntel VPU (NPU)Compute+Copy110000AI 추론용 NPU 가속기
드라이버별 소스 위치: 각 드라이버의 drm_sched 통합 코드는 drivers/gpu/drm/<드라이버>/에 위치합니다. Job 구현 파일은 보통 *_job.c 또는 *_sched.c로 명명됩니다. 새로운 GPU 드라이버를 작성할 때는 panfrost의 구현(panfrost_job.c)이 가장 단순하여 참고하기 좋습니다.

성능 튜닝과 모범 사례

drm_sched 기반 GPU 드라이버의 성능을 최적화하기 위한 핵심 파라미터와 기법을 정리합니다.

튜닝 파라미터

파라미터영향너무 작으면너무 크면
hw_submissionGPU 파이프라인 깊이GPU idle 시간 증가TDR 지연(Latency) 증가, 메모리 사용 증가
timeouthang 감지 속도긴 Job이 오탐으로 리셋실제 hang 시 사용자 대기 시간 증가
num_sched_list부하 분산 범위단일 엔진 병목entity 마이그레이션 오버헤드(Overhead)
Job 크기 (IB length)GPU 활용률Job 제출 오버헤드 증가tail latency 증가, 공정성 저하

실전 튜닝 팁

# amdgpu Job 타임아웃 조정 (모듈 파라미터)
echo 30000 > /sys/module/amdgpu/parameters/lockup_timeout

# amdgpu 스케줄러별 pending Job 수 모니터링
for f in /sys/kernel/debug/dri/0/amdgpu_gpu_sched_*; do
    echo "=== $(basename $f) ==="
    cat "$f"
done

# GPU 엔진 사용률 확인 (amdgpu)
cat /sys/class/drm/card0/device/gpu_busy_percent

# VRAM/GTT 사용량 확인 (Job 메모리 압박 진단)
cat /sys/kernel/debug/dri/0/amdgpu_gem_info | head -20

# i915 GuC 스케줄러 통계
cat /sys/kernel/debug/dri/0/gt0/uc/guc_slpc_info
AI/ML 워크로드 최적화: ROCm이나 CUDA 같은 GPGPU 워크로드는 보통 단일 프로세스가 compute 엔진을 독점합니다. 이 경우 hw_submission을 높여 GPU 파이프라인을 충분히 채우는 것이 중요합니다. 반면 데스크톱 환경에서는 여러 프로세스가 GFX 엔진을 공유하므로 공정성을 위해 적절한 값을 유지합니다.

DMA-BUF 동기화와 implicit fence

GPU 간, 또는 GPU와 디스플레이 컨트롤러 간 버퍼를 공유할 때 dma-buf의 implicit fence가 drm_sched의 Job 의존성으로 자동 추가됩니다. 이를 통해 한 GPU가 렌더링 중인 버퍼를 다른 GPU가 읽는 것을 방지합니다.

/* DMA-BUF shared/exclusive fence와 drm_sched 연동 */

/* 1. import된 DMA-BUF의 fence를 Job 의존성에 추가 */
struct dma_buf *dmabuf = bo->base.import_attach->dmabuf;
struct dma_resv *resv = dmabuf->resv;

/* 읽기 접근: exclusive(write) fence만 대기 */
drm_sched_job_add_resv_dependencies(&job->base, resv,
                                    DMA_RESV_USAGE_WRITE);

/* 쓰기 접근: 모든(read+write) fence 대기 */
drm_sched_job_add_resv_dependencies(&job->base, resv,
                                    DMA_RESV_USAGE_READ);

/* 2. Job 완료 fence를 DMA-BUF에 기록 */
dma_resv_add_fence(resv,
                  &job->base.s_fence->finished,
                  DMA_RESV_USAGE_WRITE);

/* 이후 다른 드라이버(디스플레이, 비디오 등)가
 * 같은 DMA-BUF를 사용할 때 이 fence를 자동으로 대기 */
GPU A (렌더링) drm_sched Job #1 write fence 생성 DMA-BUF (shared buffer) dma_resv: [write_fence, read_fence] GPU B (AI 추론) drm_sched Job #2 read — write fence 대기 fence 기록 fence 확인 디스플레이 (KMS) atomic commit — write fence 대기 implicit sync

진단 — 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 추적

DRM 스케줄러 작업 흐름 분석

drm_sched의 핵심 경로는 유저스페이스 Job 제출부터 GPU 실행 완료까지 이어지는 파이프라인입니다. drm_sched_entity_push_job()으로 entity 큐에 진입한 Job이 스케줄러 메인 루프 drm_sched_main()에 의해 꺼내져 드라이버의 run_job()으로 GPU에 제출되고, 완료 인터럽트가 fence를 시그널하는 전체 흐름을 추적합니다.

유저스페이스 ioctl(DRM_IOCTL_*) submit entity_push_job() spsc_queue에 Job 추가 wake_up drm_sched_main() — kthread 메인 루프 ① 의존성 해소 dep fence 대기 ② select_entity 우선순위 RR 선택 ③ pop_job entity에서 Job 추출 ops->run_job() drm_sched_backend_ops.run_job() GPU 링 버퍼에 커맨드 기록 → hw_fence 반환 GPU 실행 GPU Hardware 커맨드 실행 IRQ fence_finished signal free_job() · 유저 통보 timeout? TDR
코드 설명

위 다이어그램은 drm_sched의 전체 작업 흐름을 보여줍니다:

  1. 유저스페이스 제출 — DRM ioctl을 통해 GPU 커맨드 버퍼가 커널로 전달됩니다.
  2. entity_push_job() — Job이 entity의 SPSC(Single-Producer Single-Consumer) 큐에 추가되고, 스케줄러 kthread를 깨웁니다.
  3. drm_sched_main() 루프 — ① 의존 fence가 signal될 때까지 대기, ② 우선순위별 라운드 로빈으로 entity 선택, ③ 선택된 entity에서 Job을 꺼냅니다.
  4. run_job() — 드라이버 백엔드가 GPU 링 버퍼에 커맨드를 기록하고 hw_fence를 반환합니다.
  5. fence signal — GPU 완료 인터럽트가 hw_fence를 시그널하면, drm_sched가 finished fence를 시그널하고 free_job()으로 정리합니다.

struct drm_gpu_scheduler 필드별 해설

struct drm_gpu_scheduler는 하나의 GPU 엔진(예: GFX, Compute, SDMA)을 관리하는 핵심 구조체입니다. 커널 소스 include/drm/gpu_scheduler.h에서 정의되며, 각 필드가 스케줄러의 어떤 측면을 제어하는지 분석합니다.

/* include/drm/gpu_scheduler.h — drm_gpu_scheduler 주요 필드 */
struct drm_gpu_scheduler {
    /* ── 드라이버 인터페이스 ── */
    const struct drm_sched_backend_ops  *ops;
        /* run_job, timedout_job, free_job 콜백 테이블.
         * 드라이버마다 GPU HW 접근 방식이 다르므로 ops로 추상화 */

    uint32_t                            hw_submission_limit;
        /* GPU에 동시에 보낼 수 있는 최대 Job 수.
         * hw_rq_count가 이 값에 도달하면 새 Job 제출 차단 */

    const char                          *name;
        /* 디버그/tracepoint용 이름 (예: "gfx", "comp_1.0.0") */

    /* ── 우선순위 큐 ── */
    struct drm_sched_rq                  sched_rq[DRM_SCHED_PRIORITY_COUNT];
        /* KERNEL(0), HIGH(1), NORMAL(2), LOW(3) 4개 run-queue.
         * 높은 우선순위 큐부터 순회하며 entity를 선택 */

    /* ── kthread 관리 ── */
    struct task_struct                   *thread;
        /* drm_sched_main()을 실행하는 커널 스레드.
         * drm_sched_init() 시 kthread_run()으로 생성 */

    wait_queue_head_t                    wake_up_worker;
        /* kthread가 sleep하는 대기 큐.
         * push_job이나 fence signal 시 wake_up() */

    /* ── 실행 중 Job 추적 ── */
    struct list_head                      pending_list;
        /* GPU에 제출되어 완료 대기 중인 Job 목록.
         * TDR이 이 목록의 첫 번째 Job을 타임아웃 대상으로 판별 */

    spinlock_t                           job_list_lock;
        /* pending_list 보호. IRQ 컨텍스트에서도 접근하므로 spinlock */

    atomic_t                             hw_rq_count;
        /* 현재 GPU에 제출된 Job 수.
         * hw_submission_limit와 비교하여 flow control */

    /* ── 타임아웃 / TDR ── */
    long                                  timeout;
        /* Job 완료 타임아웃 (jiffies 단위).
         * 0이면 타임아웃 비활성화 (무한 대기) */

    struct delayed_work                   work_tdr;
        /* 타임아웃 시 drm_sched_job_timedout() 실행.
         * 첫 Job 제출 시 schedule_delayed_work()로 등록 */

    unsigned int                          hang_limit;
        /* 연속 hang 허용 횟수. 초과 시 entity를 guilty로 마킹 */

    atomic_t                             _score;
        /* 부하 점수 — entity 마이그레이션 시 가장 낮은 스케줄러 선택 */

    bool                                  ready;
        /* false이면 스케줄러 일시 중단 (drm_sched_stop 상태) */
};
코드 설명

drm_gpu_scheduler의 필드는 크게 5가지 역할 그룹으로 나뉩니다:

  • 드라이버 인터페이스ops가 GPU 하드웨어 접근을 추상화합니다. hw_submission_limit은 GPU 링 버퍼 깊이에 맞춰 설정합니다 (amdgpu는 보통 256).
  • 우선순위 큐sched_rq[] 배열이 4단계 우선순위를 구현합니다. 스케줄러 루프는 인덱스 0(KERNEL)부터 순회하므로 높은 우선순위 entity가 먼저 실행됩니다.
  • kthread 관리threadwake_up_worker가 메인 루프의 실행과 sleep/wakeup을 제어합니다.
  • 실행 중 Job 추적pending_list는 FIFO 순서로 Job을 유지하며, TDR의 타임아웃 판정 기준이 됩니다. hw_rq_counthw_submission_limit에 도달하면 kthread는 sleep합니다.
  • 타임아웃/TDRwork_tdr delayed_work가 timeout jiffies 후에 발화하여 drm_sched_job_timedout()를 호출합니다.

struct drm_sched_entity 필드별 해설

drm_sched_entity는 하나의 DRM 컨텍스트(프로세스 또는 GPU 채널)를 나타내며, 해당 컨텍스트에서 제출된 Job들의 큐와 스케줄링 속성을 관리합니다.

/* include/drm/gpu_scheduler.h — drm_sched_entity 주요 필드 */
struct drm_sched_entity {
    /* ── 큐 연결 ── */
    struct list_head              list;
        /* drm_sched_rq의 entities 리스트에 연결.
         * 라운드 로빈 스케줄링의 순환 노드 */

    struct drm_sched_rq          *rq;
        /* 현재 소속된 run-queue 포인터.
         * entity 마이그레이션 시 변경 */

    struct drm_gpu_scheduler    **sched_list;
        /* 사용 가능한 스케줄러 배열 (복수 엔진 부하 분산용).
         * num_sched_list == 1이면 고정 엔진 */

    unsigned int                 num_sched_list;

    /* ── Job 큐 ── */
    struct spsc_queue             job_queue;
        /* 단일 생산자(유저 스레드) - 단일 소비자(sched kthread) 큐.
         * lock-free로 push/pop 가능 */

    /* ── 우선순위 ── */
    enum drm_sched_priority      priority;
        /* KERNEL / HIGH / NORMAL / LOW.
         * 어떤 sched_rq에 배치될지 결정 */

    /* ── 의존성 추적 ── */
    struct dma_fence             *dependency;
        /* 현재 head job이 대기 중인 외부 fence.
         * signal되면 다음 루프에서 Job을 pop */

    struct dma_fence_cb           cb;
        /* dependency fence에 등록하는 콜백.
         * fence signal → kthread wake_up 트리거 */

    /* ── 실행 이력 ── */
    struct dma_fence             *last_scheduled;
        /* 마지막으로 GPU에 제출한 Job의 fence.
         * 같은 entity의 Job은 순서 보장이 필요하므로
         * 이전 fence를 다음 Job의 암묵적 의존성으로 추가 */

    atomic_t                     *guilty;
        /* GPU hang을 유발한 entity 표시.
         * atomic_read(guilty) == 1이면 이후 Job 즉시 실패 */

    uint64_t                     fence_seq;
        /* fence 시퀀스 번호 — Job마다 증가.
         * drm_sched_fence의 seqno 생성에 사용 */

    struct completion             entity_idle;
        /* entity 큐가 빌 때 signal. entity 해제 시
         * wait_for_completion()으로 모든 Job 완료 대기 */
};
코드 설명

drm_sched_entity의 설계 핵심은 순서 보장부하 분산의 균형입니다:

  • spsc_queue — lock-free 큐로 유저 스레드(producer)와 sched kthread(consumer) 간 경합을 최소화합니다.
  • last_scheduled — 같은 entity의 Job은 항상 제출 순서대로 실행되어야 합니다. 이전 Job의 fence를 다음 Job의 암묵적 의존성으로 추가하여 GPU 내에서도 순서를 보장합니다.
  • sched_list / num_sched_list — 복수 Compute 엔진이 있을 때, entity는 가장 부하가 낮은 스케줄러로 마이그레이션됩니다. drm_sched_entity_select_rq()_score 기반으로 최적 스케줄러를 선택합니다.
  • guilty — TDR로 GPU 리셋이 발생하면, 원인 entity에 guilty를 설정합니다. guilty entity의 이후 Job은 run_job() 호출 없이 즉시 에러 fence를 signal합니다.
  • entity_idledrm_sched_entity_destroy()에서 모든 대기 중 Job이 완료될 때까지 차단하는 데 사용합니다.

struct drm_sched_job 필드별 해설

drm_sched_job은 GPU에 제출할 개별 작업을 나타냅니다. 드라이버는 이 구조체를 자체 Job 구조체에 임베딩하여 사용합니다 (예: struct amdgpu_job).

/* include/drm/gpu_scheduler.h — drm_sched_job 주요 필드 */
struct drm_sched_job {
    struct spsc_node              queue_node;
        /* entity의 spsc_queue 내 노드.
         * push_job()에서 큐에 연결, pop_job()에서 제거 */

    struct list_head              list;
        /* scheduler의 pending_list 노드.
         * run_job() 호출 전에 pending_list에 추가,
         * fence signal 후 제거 */

    struct drm_gpu_scheduler     *sched;
        /* 이 Job이 실행되는 스케줄러 인스턴스.
         * entity에서 pop할 때 entity->rq->sched로 설정 */

    struct drm_sched_fence       *s_fence;
        /* scheduled + finished 두 fence를 포함하는 구조체.
         * drm_sched_job_arm()에서 할당 */

    struct drm_sched_entity      *entity;
        /* 이 Job을 제출한 entity.
         * drm_sched_job_init()에서 설정 */

    uint64_t                     id;
        /* 전역 Job ID (atomic64_inc).
         * tracepoint, debugfs 로그에서 Job 식별용 */

    struct list_head              dependencies;
        /* 외부 fence 의존성 목록.
         * drm_sched_job_add_dependency()로 추가.
         * 모든 fence가 signal되어야 run_job() 호출 */

    union {
        struct dma_fence_cb       finish_cb;
        struct work_struct        work;
    };
        /* finish_cb: hw_fence signal → drm_sched_job_done() 콜백
         * work: free_job()을 workqueue에서 비동기 실행 */
};
코드 설명

drm_sched_job은 두 개의 리스트에 동시에 연결됩니다:

  • queue_node → entity.job_queue — entity 내부에서 FIFO 순서로 대기합니다. 스케줄러가 이 entity를 선택하면 spsc_queue_pop()으로 꺼냅니다.
  • list → scheduler.pending_list — GPU에 제출된 후 완료 대기 중인 Job을 시간순으로 추적합니다. TDR의 타임아웃 판정 기준이 됩니다.

s_fencedrm_sched_fence 구조체로, scheduledfinished 두 단계의 fence를 제공합니다. 이 이중 fence 설계를 통해 "GPU에 제출됨"과 "GPU에서 실행 완료"를 구분할 수 있습니다.

dependencies 리스트에는 다른 엔진이나 다른 프로세스의 fence가 들어갑니다. 예를 들어 3D 렌더링 결과를 비디오 인코딩하려면, 3D Job의 finished fence를 비디오 Job의 의존성으로 추가합니다.

struct drm_sched_fence — scheduled/finished fence 쌍

drm_sched_fence는 하나의 Job에 대해 두 단계의 동기화 포인트를 제공합니다. 이 이중 fence 설계가 drm_sched 동기화의 핵심입니다.

/* include/drm/gpu_scheduler.h — drm_sched_fence */
struct drm_sched_fence {
    struct dma_fence             scheduled;
        /* run_job() 호출 직전에 signal.
         * "이 Job이 GPU에 제출되었다"는 의미.
         * 같은 entity의 다음 Job이 이 fence를 대기 */

    struct dma_fence             finished;
        /* GPU 실행 완료 후 signal.
         * hw_fence가 signal → finished도 signal.
         * 유저스페이스에 반환하는 fence가 이것 */

    struct dma_fence            *parent;
        /* run_job()이 반환한 hw_fence 포인터.
         * hw_fence signal → finished signal 체이닝 */

    struct drm_gpu_scheduler    *sched;
        /* 이 fence가 속한 스케줄러 */

    struct drm_sched_fence      *owner;
        /* refcount 관리용 자기 참조 */
};
시간 → push_job entity 큐 진입 scheduled signal run_job() 직전 s_fence->scheduled GPU 실행 GPU 처리 중 IRQ finished signal 유저에게 통보 s_fence->finished 다음 entity Job의 순서 의존성 syncobj / out_fence 유저스페이스 반환
코드 설명

drm_sched_fence의 이중 fence 설계는 중요한 최적화를 가능하게 합니다:

  • scheduled fencedrm_sched_main()run_job()을 호출하기 직전에 signal합니다. 같은 entity의 다음 Job은 이전 Job의 scheduled fence가 signal되면 entity 큐에서 꺼낼 수 있습니다. GPU 실행이 끝나기 전에도 다음 Job을 GPU에 파이프라이닝할 수 있어 처리량이 향상됩니다.
  • finished fence — GPU가 실제로 작업을 완료하고 인터럽트를 발생시켜 hw_fence가 signal되면, 그에 연결된 finished fence도 signal됩니다. 유저스페이스에 반환하는 syncobj나 out_fence는 이 finished fence입니다.
  • parent (hw_fence)run_job()이 반환하는 드라이버별 fence입니다. drm_sched_fence_set_parent()로 연결하면 hw_fence signal 시 자동으로 finished가 signal됩니다.

drm_sched_main() 함수 구현 분석

drm_sched_main()은 drm_sched의 심장부입니다. 커널 스레드로 실행되며, Job을 entity에서 꺼내 GPU에 제출하는 무한 루프를 구현합니다. drivers/gpu/drm/scheduler/sched_main.c의 핵심 로직을 단순화하여 분석합니다.

/* drivers/gpu/drm/scheduler/sched_main.c — 핵심 루프 (단순화) */
static int drm_sched_main(void *param)
{
    struct drm_gpu_scheduler *sched = param;
    struct drm_sched_entity  *entity;
    struct drm_sched_job     *sched_job;
    struct dma_fence          *fence;
    int r;

    while (!kthread_should_stop()) {
        /* ① hw_submission_limit에 여유가 생길 때까지 sleep */
        wait_event(sched->wake_up_worker,
            (drm_sched_can_queue(sched) &&
             (entity = drm_sched_select_entity(sched))) ||
            kthread_should_stop());

        if (kthread_should_stop())
            break;

        /* ② entity에서 Job 꺼내기 */
        sched_job = drm_sched_entity_pop_job(entity);
        if (!sched_job)
            continue;

        /* ③ scheduled fence signal — "GPU에 제출 예정" */
        drm_sched_fence_scheduled(sched_job->s_fence, entity);

        /* ④ pending_list에 추가 (타임아웃 추적용) */
        spin_lock(&sched->job_list_lock);
        list_add_tail(&sched_job->list, &sched->pending_list);
        spin_unlock(&sched->job_list_lock);

        /* ⑤ 타임아웃 타이머 시작 (첫 Job인 경우) */
        drm_sched_start_timeout(sched);

        /* ⑥ 드라이버 run_job() 호출 → GPU에 커맨드 제출 */
        fence = sched->ops->run_job(sched_job);

        /* ⑦ hw_fence를 s_fence.finished에 연결 */
        if (!IS_ERR_OR_NULL(fence)) {
            drm_sched_fence_set_parent(sched_job->s_fence, fence);
            r = dma_fence_add_callback(fence,
                    &sched_job->finish_cb,
                    drm_sched_job_done_cb);
            if (r == -ENOENT)
                /* fence 이미 signal됨 → 즉시 완료 처리 */
                drm_sched_job_done(sched_job, fence->error);
            dma_fence_put(fence);
        } else {
            /* fence 없음 또는 에러 → 즉시 완료 */
            drm_sched_job_done(sched_job,
                IS_ERR(fence) ? PTR_ERR(fence) : 0);
        }

        atomic_inc(&sched->hw_rq_count);
    }
    return 0;
}
코드 설명

drm_sched_main()의 각 단계를 상세히 분석합니다:

  1. ① wait_event() — 두 조건이 모두 만족할 때까지 sleep합니다:
    • drm_sched_can_queue(): hw_rq_count < hw_submission_limit 확인. GPU 파이프라인에 여유 슬롯이 있어야 합니다.
    • drm_sched_select_entity(): 우선순위 높은 run-queue부터 순회하여 Job이 준비된 entity를 찾습니다. 라운드 로빈으로 같은 우선순위의 entity들에게 공정한 기회를 줍니다.
  2. ② pop_job() — 선택된 entity의 spsc_queue에서 head Job을 꺼냅니다. 이때 Job의 의존 fence가 모두 signal되었는지 확인합니다.
  3. ③ scheduled fence signal — 같은 entity의 다음 Job이 entity 큐에서 나올 수 있게 됩니다. GPU 파이프라이닝의 핵심입니다.
  4. ④ pending_list 추가 — spinlock으로 보호합니다. IRQ 컨텍스트의 fence signal에서도 이 리스트에 접근할 수 있기 때문입니다.
  5. ⑤ 타임아웃 시작 — pending_list에 처음 Job이 들어가면 schedule_delayed_work(&sched->work_tdr, timeout)을 호출합니다.
  6. ⑥ run_job() — 드라이버가 GPU 링 버퍼에 커맨드를 기록하고, GPU가 완료 시 signal할 hw_fence를 반환합니다.
  7. ⑦ fence 연결 — hw_fence에 콜백을 등록합니다. GPU 완료 인터럽트 → hw_fence signal → drm_sched_job_done_cb()finished fence signal → free_job() 순으로 정리됩니다.

타임아웃 처리 — drm_sched_job_timedout() 구현 분석

GPU hang 발생 시 delayed_work로 트리거되는 drm_sched_job_timedout()의 내부 동작과 GPU 리셋 복구 흐름을 커널 소스 수준에서 분석합니다.

work_tdr 발화 timeout jiffies 경과 drm_sched_stop() kthread 정지, Job 수거 ops->timedout_job() 드라이버 GPU 리셋 수행 NOMINAL drm_sched_start() kthread 재개, Job 재제출 정상 운영 복귀 pending Job 순서대로 재제출 ENODEV 스케줄러 영구 중단 HW 불량 판정 모든 Job 에러 fence 유저에게 -EIO 반환 entity guilty 마킹 hang_limit 초과 시
/* drivers/gpu/drm/scheduler/sched_main.c — 타임아웃 처리 (단순화) */
static void drm_sched_job_timedout(struct work_struct *work)
{
    struct drm_gpu_scheduler *sched =
        container_of(work, typeof(*sched), work_tdr.work);
    struct drm_sched_job *job;
    enum drm_gpu_sched_stat stat;

    /* pending_list의 가장 오래된(첫 번째) Job을 가져옴 */
    spin_lock(&sched->job_list_lock);
    job = list_first_entry_or_null(&sched->pending_list,
                                    struct drm_sched_job, list);
    spin_unlock(&sched->job_list_lock);

    if (!job)
        return;

    /* 스케줄러 중단 — kthread park, pending Job 수거 */
    drm_sched_stop(sched, job);

    /* 드라이버 timedout_job 콜백:
     * - GPU 레지스터 덤프 수집
     * - GPU 하드웨어 리셋
     * - NOMINAL: 복구 성공 → start로 재개
     * - ENODEV: HW 복구 불가 → 영구 중단 */
    stat = sched->ops->timedout_job(job);

    if (stat == DRM_GPU_SCHED_STAT_ENODEV) {
        /* 하드웨어 불량 — 스케줄러 영구 중단 */
        drm_sched_fault(sched);
        return;
    }

    /* guilty 카운트 증가: hang_limit 초과 시 entity banned */
    if (job->s_fence->parent &&
        job->s_fence->parent->error == -ETIMEDOUT)
        atomic_inc(job->entity->guilty);

    /* 스케줄러 재개 — kthread unpark, pending Job 재제출 */
    drm_sched_start(sched,
        stat != DRM_GPU_SCHED_STAT_NOMINAL);
}
코드 설명

타임아웃 처리의 핵심 절차를 단계별로 분석합니다:

  1. 타임아웃 감지delayed_worktimeout jiffies(기본 10초) 후에 발화합니다. pending_list의 첫 번째 Job이 가장 오래 실행 중인 Job이므로 타임아웃 대상입니다.
  2. drm_sched_stop() — kthread를 park하여 새 Job 제출을 차단하고, pending_list의 모든 Job을 수거합니다. 이 시점에서 GPU에는 더 이상 새 커맨드가 전송되지 않습니다.
  3. timedout_job() 콜백 — 드라이버가 GPU 하드웨어 리셋을 수행합니다. amdgpu의 경우 amdgpu_device_gpu_recover()가 ASIC 레벨 리셋을 실행하고, 링 버퍼를 재초기화합니다.
  4. 복구 분기:
    • NOMINAL — 리셋 성공. drm_sched_start()가 kthread를 unpark하고, 수거한 Job들의 scheduled fence를 다시 signal하여 재제출 가능 상태로 만듭니다.
    • ENODEV — 하드웨어 복구 불가. 스케줄러가 영구 중단되며, 대기 중인 모든 Job의 fence에 에러를 signal합니다.
  5. guilty entity — 같은 entity에서 반복적으로 hang이 발생하면(hang_limit 초과), 해당 entity를 guilty로 마킹합니다. guilty entity의 이후 Job은 GPU에 제출하지 않고 즉시 에러를 반환합니다.

일반적인 문제와 디버깅(Debugging) 가이드

drm_sched 관련 문제는 주로 GPU hang, fence 타임아웃, 메모리 부족의 형태로 나타납니다. dmesg와 debugfs를 활용한 체계적인 진단 방법을 정리합니다.

증상가능한 원인dmesg 키워드진단 방법
화면 멈춤 후 복구 GPU hang → TDR 리셋 성공 GPU reset succeeded drm_sched_job_timedout tracepoint 확인
화면 완전 멈춤 GPU hang → TDR 리셋 실패 amdgpu: GPU reset failed ENODEV 반환 확인, 전체 GPU 리셋 필요
Job 제출 느림 fence 의존성 대기, BO eviction 없음 (성능 저하만) ftrace drm_sched_jobdrm_run_job 시간 측정
VRAM 부족 에러 BO 할당 실패, Job이 메모리를 해제 못 함 TTM_PL_VRAM allocation failed amdgpu_gem_info, amdgpu_vram_mm debugfs 확인
fence 순서 역전 드라이버 fence signaling 버그 fence not signaled in time CONFIG_DMA_FENCE_TRACE=y 빌드 후 추적
# GPU hang 발생 시 종합 진단 스크립트

# 1. dmesg에서 GPU 관련 에러 수집
dmesg | grep -iE "drm|amdgpu|i915|gpu|fence|sched" | tail -50

# 2. 현재 스케줄러 상태 스냅샷
cat /sys/kernel/debug/dri/0/amdgpu_gpu_sched 2>/dev/null

# 3. GPU 레지스터 덤프 (hang 원인 분석)
cat /sys/kernel/debug/dri/0/amdgpu_wave_status 2>/dev/null

# 4. VRAM/GTT 사용 현황
cat /sys/kernel/debug/dri/0/amdgpu_vram_mm 2>/dev/null

# 5. GPU 온도 (열 스로틀링 가능성)
cat /sys/class/drm/card0/device/hwmon/hwmon*/temp1_input 2>/dev/null
fence signaling 규칙: dma_fence는 반드시 유한 시간 내에 signal되어야 합니다. dma_fence_wait()에서 영원히 대기하면 전체 디스플레이 파이프라인이 멈출 수 있습니다. 드라이버에서 fence를 생성했으면, 에러 경로에서도 반드시 dma_fence_signal()을 호출해야 합니다. 이 규칙 위반은 커널에서 WARN_ON을 발생시킵니다.

멀티 링(Multi-Ring) / 멀티 엔진(Multi-Engine) 스케줄링

현대 GPU는 단일 실행 엔진이 아닙니다. 그래픽스(GFX), 컴퓨트(Compute), DMA/SDMA, 비디오 인코딩(VCE/VCN), 비디오 디코딩(UVD/VCN) 등 복수의 하드웨어 엔진을 보유하며, 각 엔진마다 독립된 drm_gpu_scheduler 인스턴스가 할당됩니다.

엔진별 스케줄러 인스턴스 모델

amdgpu 드라이버를 기준으로, Navi 10 GPU는 다음과 같은 링 구성을 가집니다.

엔진 유형링 개수스케줄러 인스턴스용도타임아웃(ms)
GFX (Graphics)1adev->gfx.gfx_ring[0].sched3D 렌더링, 셰이더 실행10,000
Compute8adev->gfx.compute_ring[0..7].schedGPGPU 컴퓨팅(CUDA/ROCm)60,000
SDMA2adev->sdma.instance[0..1].ring.schedBO 복사, 페이지 마이그레이션10,000
VCN Decode1adev->vcn.inst[0].ring_dec.schedH.264/H.265 디코딩10,000
VCN Encode2adev->vcn.inst[0].ring_enc[0..1].sched비디오 인코딩10,000
JPEG1adev->jpeg.inst[0].ring_dec.schedJPEG 하드웨어 디코드10,000

엔진 간 동기화

서로 다른 엔진의 Job 사이에 의존성이 있으면 dma_fence를 통해 동기화합니다. 예를 들어, Compute 엔진에서 계산한 결과를 GFX 엔진에서 렌더링하려면, Compute Job의 완료 fence를 GFX Job의 의존 fence로 등록합니다.

/* 크로스 엔진 의존성 예시: Compute → GFX */

/* 1. Compute 엔진에 Job A 제출 → finished fence 획득 */
struct dma_fence *compute_fence = job_a->s_fence->finished.fence;

/* 2. GFX 엔진 Job B에 의존성 추가 */
int ret = drm_sched_job_add_dependency(&job_b->base, compute_fence);
if (ret)
    return ret;

/* 3. GFX Job B 제출 — compute_fence가 signal될 때까지 대기 */
drm_sched_entity_push_job(&job_b->base);

/*
 * 실행 순서:
 * Compute ring: [Job A 실행] → fence signal
 * GFX ring:     [대기...........] → [Job B 실행]
 *
 * 두 엔진은 병렬로 다른 Job을 처리할 수 있지만,
 * Job B는 반드시 Job A 완료 후에만 실행됨
 */

Entity의 멀티 스케줄러 바인딩

하나의 drm_sched_entity는 초기화 시 복수의 스케줄러 인스턴스를 후보로 받을 수 있습니다. 이를 통해 동일 유형의 여러 엔진(예: Compute ring 0~7) 사이에서 부하 분산(Load Balancing)이 가능합니다.

/* Compute entity를 8개 Compute ring에 분산 바인딩 */
struct drm_gpu_scheduler *sched_list[8];
for (int i = 0; i < 8; i++)
    sched_list[i] = &adev->gfx.compute_ring[i].sched;

/* entity 초기화 — 8개 스케줄러 중 하나가 선택됨 */
drm_sched_entity_init(&entity, DRM_SCHED_PRIORITY_NORMAL,
                      sched_list, 8, NULL);

/*
 * drm_sched_entity_init() 내부:
 * - num_sched_list > 1이면 가장 적은 Job을 가진 스케줄러 선택
 * - entity->rq = &sched->sched_rq[priority]
 * - Job 제출 시 해당 스케줄러의 워커 스레드가 처리
 */
멀티 엔진 GPU 스케줄러 아키텍처 사용자 공간 (Mesa / ROCm / VA-API) amdgpu_cs_ioctl() → drm_sched_entity_push_job() GFX Scheduler drm_gpu_scheduler #0 Priority: K | H | N | L hw_submission_limit: 2 timeout: 10,000 ms Compute Scheduler ×8 drm_gpu_scheduler #1..#8 Priority: K | H | N | L Entity 부하 분산 (RR) timeout: 60,000 ms SDMA Scheduler ×2 drm_gpu_scheduler #9..#10 BO copy / page migration timeout: 10,000 ms VCN/JPEG Sched drm_gpu_scheduler #11..#14 Decode / Encode / JPEG timeout: 10,000 ms fence run_job() run_job() run_job() run_job() 하드웨어 링 버퍼 (GPU Command Processor) GFX Ring 0 CP 0 CP 1 ... CP 7 SDMA 0 SDMA 1 VCN Dec VCN Enc GPU Hardware (Shader Array, TMU, ROP, ...) TDR (Timeout Detection Recovery) — 각 스케줄러 인스턴스가 독립적으로 타임아웃 감시
엔진별 독립 TDR:drm_gpu_scheduler 인스턴스는 자체 delayed_work로 타임아웃을 감시합니다. Compute 엔진이 hang되더라도 GFX 엔진의 렌더링은 영향받지 않으며, Compute 엔진만 리셋할 수 있습니다(soft reset 지원 시). 그러나 하드웨어 제약으로 전체 GPU 리셋(full reset)이 필요한 경우, 모든 스케줄러가 일시 정지됩니다.

배치 제출(Batch Submission)과 파이프라이닝(Pipelining)

GPU 성능을 극대화하려면 GPU가 항상 바쁘도록(busy) 유지해야 합니다. CPU 측에서 Job을 준비하는 동안 GPU가 유휴(idle) 상태가 되면 전체 처리량(Throughput)이 감소합니다. hw_submission_limit과 backlog 큐를 활용한 파이프라이닝이 핵심입니다.

hw_submission_limit 동작

/* drm_sched_main() 워커 스레드의 Job 디스패치 루프 */

while (!kthread_should_stop()) {
    /* 1. HW 큐에 여유가 있는지 확인 */
    if (atomic_read(&sched->hw_rq_count) >= sched->hw_submission_limit)
        wait_event(sched->wake_up_worker, ...);

    /* 2. entity에서 다음 Job 선택 (라운드 로빈) */
    entity = drm_sched_select_entity(sched);
    if (!entity)
        continue;

    sched_job = drm_sched_entity_pop_job(entity);

    /* 3. 의존 fence 대기 완료 확인 */
    if (sched_job->s_fence->parent)
        dma_fence_wait(sched_job->s_fence->parent);

    /* 4. 드라이버의 run_job 콜백 호출 → HW 링에 제출 */
    fence = sched->ops->run_job(sched_job);
    atomic_inc(&sched->hw_rq_count);

    /* 5. TDR 타이머 시작 */
    drm_sched_start_timeout(sched);
}
파라미터기본값영향조정 가이드
hw_submission_limit드라이버 결정 (amdgpu: 2)HW 큐에 동시에 있을 수 있는 Job 수높이면 GPU 유휴 감소, 낮추면 TDR 반응 빠름
timeout10,000 ms (GFX)Job 완료 대기 최대 시간ML 워크로드는 60초 이상 필요 가능
hang_limit0 (무제한)연속 hang 허용 횟수, 초과 시 entity ban프로덕션 환경에서 3~5 설정 권장
num_rqsDRM_SCHED_PRIORITY_COUNT (4)우선순위 큐 수커널 API에 의해 고정

파이프라이닝 타임라인

이상적인 파이프라이닝에서는 CPU가 Job N+2를 준비하는 동안 GPU는 Job N을 실행하고, Job N+1은 HW 큐에서 대기합니다.

GPU Job 파이프라이닝 (hw_submission_limit = 2) 시간 → t0 t1 t2 t3 t4 CPU (push_job) Job A 준비 Job B 준비 Job C 준비 Job D 준비 HW 큐 (run_job) Job A 대기 Job B 대기 Job C 대기 Job D 대기 GPU (실행) Job A 실행 Job B 실행 Job C 실행 Fence (signal) A done B done C done GPU가 유휴 상태 없이 연속 실행 — CPU 준비와 GPU 실행이 겹침(overlap)
파이프라이닝 실패 패턴: CPU가 Job을 충분히 빠르게 준비하지 못하면 GPU가 유휴 상태가 됩니다(CPU-bound). 반대로 GPU가 너무 느리면 backlog 큐에 Job이 쌓여 메모리 압박이 발생합니다(GPU-bound). ftracedrm_sched_jobdrm_run_job 이벤트 사이의 시간 차이를 측정하면 병목(Bottleneck)을 식별할 수 있습니다.

드라이버별 drm_sched 구현

drm_sched는 공통 프레임워크이지만, 각 드라이버는 하드웨어 특성에 맞게 drm_sched_backend_ops 콜백을 다르게 구현합니다. 주요 드라이버의 구현 차이를 비교합니다.

amdgpu 구현 상세

static const struct drm_sched_backend_ops amdgpu_sched_ops = {
    .run_job    = amdgpu_job_run,
    .timedout_job = amdgpu_job_timedout,
    .free_job   = amdgpu_job_free_cb,
};

/* amdgpu_job_run: IB(Indirect Buffer)를 HW 링에 emit */
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 = to_amdgpu_ring(sched_job->sched);

    /* VM 페이지 테이블 업데이트 (필요 시) */
    amdgpu_vm_flush(ring, job);

    /* IB를 링 버퍼에 기록 */
    for (i = 0; i < job->num_ibs; i++)
        amdgpu_ib_schedule(ring, job->num_ibs, job->ibs, ...);

    /* 도어벨(doorbell)로 GPU에 알림 */
    amdgpu_ring_commit(ring);

    return fence;
}

/* amdgpu_job_timedout: GPU hang 시 리셋 절차 */
static enum drm_gpu_sched_stat amdgpu_job_timedout(
    struct drm_sched_job *s_job)
{
    /* 1. GPU 상태 스냅샷 저장 */
    amdgpu_device_gpu_recover(adev, job);
    /*
     * 내부 절차:
     * - 모든 스케줄러 정지 (drm_sched_stop)
     * - ASIC 리셋 (mode1/mode2/BACO)
     * - 모든 링 재초기화
     * - guilty entity 마킹
     * - 대기 중인 Job 재제출 (drm_sched_resubmit_jobs)
     * - 스케줄러 재시작 (drm_sched_start)
     */
    return DRM_GPU_SCHED_STAT_NOMINAL;
}

nouveau, Xe, panfrost 비교

특성amdgpui915/Xenouveaupanfrostvirtio-gpu
스케줄러 소유권드라이버(커널)GuC (FW)드라이버(커널)드라이버(커널)호스트 위임
HW 큐 수엔진별 1GuC 내부 관리채널별 1엔진별 1 (JM/CSF)가상 큐
선점 지원Mid-wave (RDNA3+)GuC 선점채널 선점CSF 기반 (v10+)없음
리셋 범위Per-ring / FullPer-enginePer-channel / FullSoft reset / Hard해당 없음
메모리 모델TTM (VRAM + GTT)TTM + LMEMTTM + VRAMGEM + SHMEM호스트 GEM
타임아웃(GFX)10,000 msGuC 내부15,000 ms500 ms30,000 ms
GPU 페이지 폴트SVM (XNACK)SVM (Xe2+)미지원미지원해당 없음

panfrost (ARM Mali) 구현 특징

panfrost는 ARM Mali GPU (Midgard/Bifrost/Valhall) 드라이버입니다. GPU 아키텍처가 x86 GPU와 크게 다르므로, drm_sched 사용 패턴도 다릅니다.

/* panfrost: Job Manager 기반 제출 */
static struct dma_fence *panfrost_job_run(
    struct drm_sched_job *sched_job)
{
    struct panfrost_job *job = to_panfrost_job(sched_job);

    /* Job descriptor chain을 GPU Job Manager에 직접 기록 */
    panfrost_mmu_flush_range(pfdev, ...);
    gpu_write(pfdev, JS_HEAD_NEXT_LO(js), lower_32_bits(jc));
    gpu_write(pfdev, JS_HEAD_NEXT_HI(js), upper_32_bits(jc));
    gpu_write(pfdev, JS_COMMAND_NEXT(js), JS_COMMAND_START);

    return fence;
}

/* 타임아웃: 짧은 500ms — 임베디드 환경 특성 */
drm_sched_init(&pfdev->js->queue[js].sched,
               &panfrost_sched_ops, NULL,
               DRM_SCHED_PRIORITY_COUNT,
               1,    /* hw_submission: 1 (Job Manager 제약) */
               0,    /* hang_limit */
               msecs_to_jiffies(500), ...);

GPU 행(Hang) 덤프 심층 분석

GPU hang이 발생하면 커널은 자동으로 레지스터 덤프와 웨이브(wave) 상태를 수집합니다. 이 정보를 해석하여 hang의 근본 원인(Root Cause)을 찾는 방법을 설명합니다.

덤프 수집

# 1. GPU devcoredump 수집 (자동 생성)
ls /sys/class/devcoredump/devcd*/
cat /sys/class/devcoredump/devcd0/data > gpu_hang_dump.bin

# 2. umr 도구로 amdgpu 레지스터 덤프 해석
umr --read mmGRBM_STATUS
umr --read mmSRBM_STATUS

# 3. wave 상태 확인 (어떤 셰이더가 멈췄는지)
cat /sys/kernel/debug/dri/0/amdgpu_wave_status

# 4. fence 상태 확인 (어떤 Job이 미완료인지)
cat /sys/kernel/debug/dri/0/amdgpu_fence_info

# 5. ring 상태 확인 (HW 포인터 위치)
cat /sys/kernel/debug/dri/0/amdgpu_ring_gfx_0

hang 원인 분류

GRBM_STATUS의미가능한 원인조치
GUI_ACTIVE=1, TA_BUSY=1Texture Array가 바쁨무한 텍스처 샘플링 루프셰이더 코드 검토
SPI_BUSY=1, SX_BUSY=1셰이더 프로세서 멈춤ALU 무한 루프, 데드락wave dump 분석
CP_BUSY=1, CP_COHERENCY=0Command Processor 멈춤잘못된 PM4 패킷IB 내용 검증
DB_BUSY=1Depth Buffer 유닛 멈춤Z-buffer 크기 초과렌더 타겟 설정 확인
전부 0GPU 전원 꺼짐PCIe 링크 다운, BACO 진입PCIe AER 로그 확인

bpftrace를 이용한 실시간 스케줄링 분석

#!/usr/bin/env bpftrace
/* GPU Job 제출~완료 레이턴시 히스토그램 */

tracepoint:drm:drm_sched_job {
    @submit[args->id] = nsecs;
}

tracepoint:drm:drm_run_job {
    if (@submit[args->id]) {
        @queue_latency_us = hist((nsecs - @submit[args->id]) / 1000);
        delete(@submit[args->id]);
    }
}

tracepoint:drm:drm_sched_process_job {
    @complete[args->fence] = nsecs;
}

END {
    printf("\n=== GPU Job 큐 대기 시간 (us) ===\n");
    print(@queue_latency_us);
    clear(@submit);
    clear(@complete);
}
GPU Hang 진단 플로우차트 GPU Hang 감지 (TDR) dmesg | grep "GPU reset" 성공 실패 amdgpu_fence_info 확인 어떤 ring의 Job이 타임아웃? GFX → 셰이더 분석 Compute → ML 워크로드 GRBM_STATUS 레지스터 확인 전부 0? → PCIe 링크 확인 lspci / AER 로그 wave dump 분석 devcoredump → umr 해석 → guilty entity 식별 → 드라이버/앱 버그 수정

DRM 스케줄러 발전 방향

drm_sched는 지속적으로 발전하고 있으며, 최근 커널 버전에서 다음과 같은 변화가 진행 중입니다.

drm_exec — 새로운 실행 프레임워크

커널 6.6에서 도입된 drm_exec은 다중 GEM 오브젝트의 잠금 획득을 안전하게 처리하는 헬퍼입니다. 기존의 ww_mutex 기반 BO 잠금 로직을 단순화합니다.

/* drm_exec 사용 예시 — 다중 BO 잠금 */
struct drm_exec exec;

drm_exec_init(&exec, DRM_EXEC_INTERRUPTIBLE_WAIT, 0);
drm_exec_until_all_locked(&exec) {
    /* 필요한 모든 BO를 잠금 시도 */
    ret = drm_exec_prepare_obj(&exec, &bo1->base, 1);
    drm_exec_retry_on_contention(&exec);
    ret = drm_exec_prepare_obj(&exec, &bo2->base, 1);
    drm_exec_retry_on_contention(&exec);
}
/* 모든 BO 잠금 획득 완료 — Job 제출 진행 */
...
drm_exec_fini(&exec);
주제현재 상태진행 방향관련 드라이버
GPU 페이지 폴트 기반 SVMamdgpu XNACK (실험적)모든 GPU에서 통일된 가상 주소 공간amdgpu, Xe
per-engine 리셋일부 드라이버 지원hang 시 다른 엔진에 영향 없는 복구i915 GuC, Xe
CSF (Command Stream Frontend)panfrost v10+GPU FW가 스케줄링 담당 (GuC 유사)panfrost (Mali Valhall+)
Vulkan Timeline Semaphoredrm_syncobj timeline유저스페이스 세밀한 동기화전 드라이버
drm_sched 공유 메모리 evictionTTM per-BO eviction스케줄러 인지 eviction 정책amdgpu, Xe
에너지 효율 스케줄링DVFS만 사용워크로드 기반 전력 상태 전환panfrost, amdgpu
커뮤니티 동향 참고: DRM 스케줄러 관련 패치는 dri-devel@lists.freedesktop.org 메일링 리스트에서 논의됩니다. drm/scheduler 태그로 Patchwork를 검색하면 최신 패치 시리즈를 추적할 수 있습니다.

참고 링크

커널 공식 문서:
외부 참고 링크:
커널 소스 참고 경로:
  • drivers/gpu/drm/scheduler/ — drm_sched 코어 (sched_main.c, sched_entity.c, sched_fence.c)
  • include/drm/gpu_scheduler.h — drm_sched API 헤더, 구조체 정의
  • drivers/gpu/drm/amd/amdgpu/amdgpu_job.c — amdgpu drm_sched 잡 구현 예시
  • drivers/gpu/drm/xe/xe_sched_job.c — xe 드라이버의 drm_sched 잡 구현
  • drivers/accel/ivpu/ivpu_job.c — NPU 드라이버의 drm_sched 활용 예시
  • drivers/gpu/drm/panfrost/panfrost_job.c — ARM Mali GPU drm_sched 활용
다음 학습 경로: