NPU 동적 주파수 조절을 통한 전력 관리: Linux cpufreq 프레임워크 활용

NPU의 전력 효율을 극대화하기 위해 실시간 워크로드에 맞춰 클럭 속도와 전압을 자동으로 조정하는 기법을 다룬다. Linux 커널의 성숙한 cpufreq 인프라를 NPU 하드웨어에 적용하여 "필요한 만큼만" 전력을 공급하는 방식을 구현하는 전체 과정을 살펴본다.

동적 주파수 조절의 전력학적 원리

반도체의 동적 전력 소비는 다음 관계식으로 표현된다:

P_dynamic ∝ C × V² × f

여기서 C는 부하 커패시턴스, V는 공급 전압, f는 동작 주파수다. 실제로 고주파 동작을 위해서는 문턱 전압 이상의 여유를 확보해야 하므로 주파수와 전압은 거의 선형적 상관관계를 가진다. 결과적으로 전력 소비는 주파수의 세제곱에 근접하게 비례하게 되어, 주파수를 절반으로 낮추면 전력을 대략 1/8까지 절감할 수 있다.

고정된 높은 주파수로 운영하면 단순한 추론 작업에서도 불필요한 전력을 낭비하고, 낮은 주파수로 고정하면 복잡한 모델 처리 시 지연 시간이 급증한다. 워크로드에 따른 적응형 주파수 제어가 이 딜레마를 해결하는 열쇠다.

cpufreq 아키텍처의 NPU 적용

Linux cpufreq 서브시스템은 세 가지 계층으로 구성되며, 이를 NPU에 재구성할 수 있다:

cpufreq 구성요소원래 역할NPU 매핑
Governor부하 패턴 분석 및 목표 주파수 산출NPU 전용 정책 모듈 (워크로드 예측 기반)
Driver플랫폼별 클럭/전압 변경 실행NPU 클럭 트리와 레귤레이터 직접 제어
Core정책 관리 및 sysfs 인터페이스NPU 장치 클래스로 확장

구현 절차: RK3588 기반 실전 예시

1단계: 하드웨어 추상화 계층 구축

먼저 NPU의 전원 도메인과 클럭 소스를 커널 리소스로 획득한다:

/* 장트리에서 정의된 NPU 자원 획득 */
struct npu_power_domain {
    struct clk *core_clk;           /* NPU 핵심 클럭 */
    struct clk *axi_clk;            /* 버스 인터페이스 클럭 */
    struct regulator *vdd_core;     /* 핵심 전원 레일 */
    struct regmap *pmucntl;         /* PMU 제어 레지스터 */
};

/* OPP(Operating Performance Point) 테이블 */
static struct npu_opp_table rk3588_npu_opp[] = {
    { .rate = 200000000,  .volt_uv = 700000,  .power_mw = 150 },
    { .rate = 400000000,  .volt_uv = 750000,  .power_mw = 380 },
    { .rate = 600000000,  .volt_uv = 800000,  .power_mw = 720 },
    { .rate = 800000000,  .volt_uv = 875000,  .power_mw = 1350 },
    { .rate = 1000000000, .volt_uv = 950000,  .power_mw = 2100 },
};

2단계: 부하 감지 엔진 구현

NPU의 실시간 활용도를 측정하기 위해 하드웨어 성능 모니터링 유닛(PMU)을 활용한다:

struct npu_pmu_counters {
    u32 mac_active_cycles;      /* MAC 어레이 활성 사이클 */
    u32 ddr_transactions;       /* DDR 메모리 접근 횟수 */
    u32 dma_busy_cycles;        /* DMA 채널 점유 시간 */
    u32 task_queue_depth;       /* 대기 중인 추론 작업 수 */
};

static u32 calculate_npu_load(struct npu_device *ndev)
{
    struct npu_pmu_counters cnt;
    u32 mac_util, mem_pressure, queue_factor;
    
    /* PMU 레지스터 일괄 샘플링 */
    npu_pmu_snapshot(ndev->base, &cnt);
    
    /* MAC 활용률: 0-100% */
    mac_util = (cnt.mac_active_cycles * 100) / PMU_SAMPLE_WINDOW;
    
    /* 메모리 압박 지수: DDR 대역폭 점유 추정 */
    mem_pressure = min(cnt.ddr_transactions / DDR_BW_THRESHOLD, 100U);
    
    /* 대기열 길이 가중치 */
    queue_factor = min(cnt.task_queue_depth * 20, 100U); /* 작업당 20% */
    
    /* 복합 부하 지수 산출 (가중 평균) */
    return (mac_util * 60 + mem_pressure * 25 + queue_factor * 15) / 100;
}

3단계: Governor 정책 엔진

부하 변화에 대응하는 히스테리시스 기반 알고리즘을 구현한다:

struct npu_governor {
    const char *name;
    unsigned int up_threshold;      /* 상향 조정 임계값 */
    unsigned int down_threshold;    /* 하향 조정 임계값 */
    unsigned int boost_duration;    /* 고성능 모드 유지 시간 */
    unsigned int slack_duration;    /* 저성능 모드 진입 지연 */
};

static int adaptive_governor(struct npu_device *ndev, unsigned int *target)
{
    struct npu_governor *gov = ndev->policy;
    unsigned int current = ndev->cur_freq;
    unsigned int load = ndev->load_indicator;
    ktime_t now = ktime_get();
    
    /* 급격한 부하 변화 방지를 위한 필터링 */
    if (abs(load - ndev->prev_load) < 10)
        return -EAGAIN;  /* 무시할 변화폭 */
    
    if (load > gov->up_threshold) {
        /* 상향 조정: 다음 높은 OPP로 */
        *target = find_next_opp(ndev, current, +1);
        ndev->boost_expire = ktime_add_ms(now, gov->boost_duration);
    } 
    else if (load < gov->down_threshold && 
             ktime_after(now, ndev->boost_expire)) {
        /* 하향 조정: 부하가 낮고 부스트 기간 만료 후 */
        *target = find_next_opp(ndev, current, -1);
    }
    else {
        /* 현재 상태 유지 */
        *target = current;
    }
    
    ndev->prev_load = load;
    return 0;
}

4단계: 안전한 주파수 전환 실행

하드웨어 손상 방지를 위해 전압-주파수 순서를 엄격히 준수한다:

static int npu_set_performance_point(struct npu_device *ndev, 
                                      struct npu_opp_table *opp)
{
    struct npu_opp_table *cur = ndev->current_opp;
    int ret;
    
    /* 미리 검증: 전압/주파수 범위 확인 */
    if (!opp_valid_for_silicon(ndev, opp))
        return -EINVAL;
    
    mutex_lock(&ndev->transition_lock);
    
    if (opp->rate > cur->rate) {
        /* 상향: 전압 먼저, 후 주파수 (안정성 확보) */
        ret = regulator_set_voltage(ndev->pd.vdd_core,
                                    opp->volt_uv, opp->volt_uv + 25000);
        if (ret) goto err_voltage;
        
        usleep_range(100, 150);  /* 전압 안정화 대기 */
        
        ret = clk_set_rate(ndev->pd.core_clk, opp->rate);
        if (ret) goto err_clock;
    } 
    else {
        /* 하향: 주파수 먼저, 후 전압 (전력 절감) */
        ret = clk_set_rate(ndev->pd.core_clk, opp->rate);
        if (ret) goto err_clock;
        
        /* 클럭 게이팅 안정화 */
        mb();  /* 메모리 배리어 */
        
        ret = regulator_set_voltage(ndev->pd.vdd_core,
                                    opp->volt_uv, opp->volt_uv + 25000);
        if (ret) goto err_voltage;
    }
    
    ndev->current_opp = opp;
    trace_npu_freq_transition(ndev, cur, opp);
    
    mutex_unlock(&ndev->transition_lock);
    return 0;

err_clock:
    /* 롤백 시도 */
    clk_set_rate(ndev->pd.core_clk, cur->rate);
err_voltage:
    regulator_set_voltage(ndev->pd.vdd_core, cur->volt_uv, 
                          cur->volt_uv + 25000);
    mutex_unlock(&ndev->transition_lock);
    return ret;
}

5단계: 사용자 공간 인터페이스

sysfs를 통해 런타임 정책 제어를 제공한다:

/* /sys/class/npu/npu0/ 아래 생성 */
static struct attribute *npu_freq_attrs[] = {
    &dev_attr_available_frequencies.attr,   /* 지원 주파수 목록 */
    &dev_attr_current_frequency.attr,       /* 현재 설정값 */
    &dev_attr_target_frequency.attr,        /* 목표 주파수 (수동) */
    &dev_attr_governor.attr,                /* 활성 정책 */
    &dev_attr_load_average.attr,              /* 1초 평균 부하 */
    &dev_attr_transition_stats.attr,        /* 주파수 변경 통계 */
    &dev_attr_thermal_throttle.attr,          /* 온도 제한 상태 */
    NULL
};

사용 예시:

# 현재 상태 확인
$ cat /sys/class/npu/npu0/current_frequency 
600000000

# Governor를 성능 우선으로 변경
$ echo performance > /sys/class/npu/npu0/governor

# 특정 주파수로 고정 (디버깅용)
$ echo 400000000 > /sys/class/npu/npu0/target_frequency

# 부하 이력 확인
$ cat /sys/class/npu/npu0/load_average
67 45 82 34 12  ...

고급 부하 예측 기법

단순한 현재 부하 측정을 넘어 지수 가중 이동 평균(EWMA)추론 작업 특성 분석을 결합한다:

struct npu_predictor {
    u32 ewma_load;          /* 지수 평활화 부하 */
    u32 trend;              /* 부하 변화 추세 */
    u32 inference_profile;  /* 모델 복잡도 힌트 */
};

static void update_prediction(struct npu_device *ndev)
{
    struct npu_predictor *pred = &ndev->pred;
    u32 instant = ndev->raw_load;
    u32 prev_ewma = pred->ewma_load;
    
    /* EWMA: α=0.3 가중치 */
    pred->ewma_load = (instant * 3 + prev_ewma * 7) / 10;
    
    /* 추세 계산: 상승/하락/안정 판단 */
    if (pred->ewma_load > prev_ewma + 15)
        pred->trend = TREND_RISING;
    else if (pred->ewma_load < prev_ewma - 15)
        pred->trend = TREND_FALLING;
    else
        pred->trend = TREND_STABLE;
    
    /* 추론 작업 힌트: 다음 작업의 예상 복잡도 */
    if (ndev->next_job)
        pred->inference_profile = analyze_model_layers(ndev->next_job);
}

실제 측정 결과

MobileNet-SSD 객체 인식 파이프라인에서의 성능 비교:

운영 모드평균 주파수평균 전력99번째 백분위 지연에너지/추론
고정 1.0GHz1000MHz2.1W12ms25.2mJ
고정 200MHz200MHz0.15W180ms27.0mJ
단순 ondemand520MHz0.78W35ms9.4mJ
예측형 adaptive480MHz0.62W22ms6.8mJ

예측형 정책이 단순 반응형 대비 28% 추가 에너지 절감을 달성하며, 지연 시간 변동성도 크게 개선했다.

핵심 주의사항

전압-주파수 순서 준수: 상향 시 전압 선반입, 하향 시 주파수 선감소가 하드웨어 안전을 보장한다. 순서가 뒤바뀌면 타이밍 위반이나 과전압 손상이 발생할 수 있다.

전이 지연 관리: 전압 레귤레이터의 슬루 레이트와 PLL 재잠금 시간을 고려하여 최소 안정화 간격을 확보해야 한다. 일반적으로 50-200μs 범위가 필요하다.

온도 연동 제어: NPU 온도 센서와 연계하여 임계 초과 시 강제 다운스케일링하는 독립적인 thermal 쓰로틀링 경로를 별도 구현한다.

추론 작업 완결성: 주파수 변경은 메모리 접근이 없는 안전 지점에서만 실행되어야 한다. 일부 NPU는 하드웨어 수준의 "frequency change ready" 신호를 제공한다.

디버깅 및 트레이싱

ftrace와 커스텀 트레이스포인트로 동작을 검증한다:

# NPU 주파수 전이 이벤트 활성화
echo 1 > /sys/kernel/debug/tracing/events/npu/freq_transition/enable

# 실시간 로그 확인
cat /sys/kernel/debug/tracing/trace_pipe | grep npu
npu-449   [002] ....   123.456789: freq_transition: 400000000 -> 600000000 (load=78, reason=boost)
npu-449   [002] ....   125.234567: freq_transition: 600000000 -> 400000000 (load=32, reason=slack)

이러한 인프라를 통해 NPU는 모바일 배터리 환경에서도 실시간 AI 추론이 가능한 전력 효율을 달성한다.

태그: NPU cpufreq devfreq power management Linux kernel

9월 21일 11:42에 게시됨