⚙️

LinuxのスケジューラをOptunaで最適化してみた

2021/02/17に公開

概要

この文書ではOSのスケジューラを扱います。
書籍『Linux Kernel Development』によれば、スケジューラとは「次にどのプロセスを実行するかを選択するプログラム」です。
LinuxはCFS(Completely Fair Scheduler)というスケジューラを使用しており、CFSは様々なチューニングパラメータを持っています。
この文書ではOptunaを用いてCFSの最適化を行います。
Linuxカーネルのビルド時間が最短になるように最適化を行います。
結論から言うと 1.8 秒だけ短縮できました。

ビルド環境

ハードウェア

項目
CPU Intel Core-i7 3770 ( 3.40GHz )
CPUコア 4 個 ( HT により 8 個 )
メモリ 16 Gbytes

ソフトウェア

項目
ホストOS Ubuntu 18.04 ( amd64 ) 4.15.0-45-generic

ビルド対象Linux

項目
kernel version Linux 5.0-rc8
tree wireless-testing
commit 9a656b08652c4975c17a4bec6271aebdebf3c37a
commit log Add localversion to identify builds from this tree

ソース

import subprocess
import errno
import time
import optuna
import signal

def signal_handler(signum, frame):
	raise Exception("Timed out")

def objective(trial):
	params = {
		'GENTLE_FAIR_SLEEPERS': trial.suggest_categorical('GENTLE_FAIR_SLEEPERS', ['GENTLE_FAIR_SLEEPERS', 'NO_GENTLE_FAIR_SLEEPERS']),
		'START_DEBIT': trial.suggest_categorical('START_DEBIT', ['START_DEBIT', 'NO_START_DEBIT']),
		'NEXT_BUDDY': trial.suggest_categorical('NEXT_BUDDY', ['NEXT_BUDDY', 'NO_NEXT_BUDDY']),
		'LAST_BUDDY': trial.suggest_categorical('LAST_BUDDY', ['LAST_BUDDY', 'NO_LAST_BUDDY']),
		'CACHE_HOT_BUDDY': trial.suggest_categorical('CACHE_HOT_BUDDY', ['CACHE_HOT_BUDDY', 'NO_CACHE_HOT_BUDDY']),
		'WAKEUP_PREEMPTION': trial.suggest_categorical('WAKEUP_PREEMPTION', ['WAKEUP_PREEMPTION', 'NO_WAKEUP_PREEMPTION']),
		'HRTICK': trial.suggest_categorical('HRTICK', ['HRTICK', 'NO_HRTICK']),
		'DOUBLE_TICK': trial.suggest_categorical('DOUBLE_TICK', ['DOUBLE_TICK', 'NO_DOUBLE_TICK']),
		'LB_BIAS': trial.suggest_categorical('LB_BIAS', ['LB_BIAS', 'NO_LB_BIAS']),
		'NONTASK_CAPACITY': trial.suggest_categorical('NONTASK_CAPACITY', ['NONTASK_CAPACITY', 'NO_NONTASK_CAPACITY']),
		'TTWU_QUEUE': trial.suggest_categorical('TTWU_QUEUE', ['TTWU_QUEUE', 'NO_TTWU_QUEUE']),
		'SIS_AVG_CPU': trial.suggest_categorical('SIS_AVG_CPU', ['SIS_AVG_CPU', 'NO_SIS_AVG_CPU']),
		'SIS_PROP': trial.suggest_categorical('SIS_PROP', ['SIS_PROP', 'NO_SIS_PROP']),
		'WARN_DOUBLE_CLOCK': trial.suggest_categorical('WARN_DOUBLE_CLOCK', ['WARN_DOUBLE_CLOCK', 'NO_WARN_DOUBLE_CLOCK']),
		'RT_PUSH_IPI': trial.suggest_categorical('RT_PUSH_IPI', ['RT_PUSH_IPI', 'NO_RT_PUSH_IPI']),
		'RT_RUNTIME_SHARE': trial.suggest_categorical('RT_RUNTIME_SHARE', ['RT_RUNTIME_SHARE', 'NO_RT_RUNTIME_SHARE']),
		'LB_MIN': trial.suggest_categorical('LB_MIN', ['LB_MIN', 'NO_LB_MIN']),
		'ATTACH_AGE_LOAD': trial.suggest_categorical('ATTACH_AGE_LOAD', ['ATTACH_AGE_LOAD', 'NO_ATTACH_AGE_LOAD']),
		'WA_IDLE': trial.suggest_categorical('WA_IDLE', ['WA_IDLE', 'NO_WA_IDLE']),
		'WA_WEIGHT': trial.suggest_categorical('WA_WEIGHT', ['WA_WEIGHT', 'NO_WA_WEIGHT']),
		'WA_BIAS': trial.suggest_categorical('WA_BIAS', ['WA_BIAS', 'NO_WA_BIAS']),

		'sched_autogroup_enabled': trial.suggest_int('sched_autogroup_enabled', 0, 1),
		'sched_cfs_bandwidth_slice_us': trial.suggest_int('sched_cfs_bandwidth_slice_us', 1, 1000000000),
		'sched_child_runs_first': trial.suggest_int('sched_child_runs_first', 0, 1),
		'sched_latency_ns': trial.suggest_int('sched_latency_ns', 100000, 1000000000),
		'sched_migration_cost_ns': trial.suggest_int('sched_migration_cost_ns', 0, 1000000000),
		'sched_min_granularity_ns': trial.suggest_int('sched_min_granularity_ns', 100000, 1000000000),
		'sched_nr_migrate': trial.suggest_int('sched_nr_migrate', 1, 128),
		'sched_rr_timeslice_ms': trial.suggest_int('sched_rr_timeslice_ms', 1, 1000),
		#'sched_rt_period_us': trial.suggest_int('sched_rt_period_us', 950000, 1000000000),
		#'sched_rt_runtime_us': trial.suggest_int('sched_rt_runtime_us', 0, 1000000000),
		'sched_schedstats': trial.suggest_int('sched_schedstats', 0, 0),
		'sched_time_avg_ms': trial.suggest_int('sched_time_avg_ms', 1, 1000),
		'sched_tunable_scaling': trial.suggest_int('sched_tunable_scaling', 0, 0),
		'sched_wakeup_granularity_ns': trial.suggest_int('sched_wakeup_granularity_ns', 0, 1000000000),
	}

	for key in params:
		if key.startswith('sched_'):
			print(key, params[key])
			with open('/proc/sys/kernel/' + key, 'w') as f:
				f.write(str(params[key]))
		else:
			print(key, params[key])
			with open('/sys/kernel/debug/sched_features', 'w') as f:
				f.write(params[key])

	build_dir = '~/git/wireless-testing'

	make_result = subprocess.run(['/usr/bin/make', 'clean'], cwd=build_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
	if make_result.returncode != 0:
		print('returncode', make_result.returncode)
		print('stderr', make_result.stderr.decode())
		exit(0)

	start_time = time.time()
	make_result = subprocess.run(['/usr/bin/make', '-j9'], cwd=build_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
	if make_result.returncode != 0:
		print('returncode', make_result.returncode)
		print('stderr', make_result.stderr.decode())
		exit(0)

	duration = time.time() - start_time
	print(duration)

	max_time = 1000.0
	if duration > max_time:
		print('duration exceeded max time', duration)
		exit(0)

	result_array.append(duration)

	return duration / max_time

def run_optuna():
	study = optuna.create_study()
	study.optimize(objective, n_trials=256)

	print('Number of finished trials: {}'.format(len(study.trials)))

	print('Best trial:')
	trial = study.best_trial

	print('  Value: {}'.format(trial.value))

	print('  Params: ')
	for key, value in trial.params.items():
		print('    {}: {}'.format(key, value))

result_array = []
run_optuna()
print(result_array)

※ sched_rt_period_usとsched_rt_runtime_usはリアルタイムプロセスに関するパラメータなので除外しました。
※ sched_schedstats は 0 が最適なのは自明なので 0 に固定しました。
※ sched_tunable_scaling はデフォルトの 1 の場合は CPU コアの数に応じてパラメータ値が変化するので 0 に固定しました。

結果

デフォルト値での実行結果: 140.1023600101471(sec)
最適化後の実行結果: 139.7200379371643(sec)
0.4 秒しか短縮できませんでした。

※ キャッシュの効果を考慮し、一度ビルドしてから測定を開始しました。
/sys/kernel/debug/sched_features の値は以下の通りです。

項目 デフォルト 最適化後
GENTLE_FAIR_SLEEPERS on on
START_DEBIT on off
NEXT_BUDDY off on
LAST_BUDDY on on
CACHE_HOT_BUDDY on on
WAKEUP_PREEMPTION on on
HRTICK off off
DOUBLE_TICK off on
LB_BIAS on on
NONTASK_CAPACITY on on
TTWU_QUEUE on on
SIS_AVG_CPU off on
SIS_PROP on off
WARN_DOUBLE_CLOCK off on
RT_PUSH_IPI on off
RT_RUNTIME_SHARE on on
LB_MIN off off
ATTACH_AGE_LOAD on off
WA_IDLE on off
WA_WEIGHT on off
WA_BIAS on off

/proc/sys/kernel/sched_* の値は以下の通りです。

項目 デフォルト 最適化後
sched_autogroup_enabled 1 0
sched_cfs_bandwidth_slice_us 5000 451661808
sched_child_runs_first 0 0
sched_latency_ns 24000000 326652418
sched_migration_cost_ns 500000 1210381
sched_min_granularity_ns 3000000 210370275
sched_nr_migrate 32 80
sched_rr_timeslice_ms 100 774
sched_rt_period_us 1000000 1000000
sched_rt_runtime_us 950000 950000
sched_schedstats 0 0
sched_time_avg_ms 1000 401
sched_tunable_scaling 1 0
sched_wakeup_granularity_ns 4000000 609092

リトライ

前述の通り、あまり良い結果は出ませんでした。
やはり全パラメータ丸投げでは探索空間が広すぎると考え、効きそうなパラメータ(下記 3 項目)に絞ってリトライします。

sched_latency_ns
sched_min_granularity_ns
sched_wakeup_granularity_ns

結果、138.32158493995667(sec) になりました。デフォルトから 1.8 秒の短縮です。誤差の範囲っぽいですね...
既に先人の皆さんによって十分最適化されているということなのでしょう。「もっとこのパラメータに絞った方が良い」等ご意見お待ちしています。

項目 デフォルト 最適化後
sched_latency_ns 24000000 458865373
sched_min_granularity_ns 3000000 217321603
sched_tunable_scaling 1 0
sched_wakeup_granularity_ns 4000000 160523018

※ sched_tunable_scaling は先程同様 0 に固定しています。

Discussion