テストステ論

高テス協会会長が, テストステロンに関する情報をお届けします.

(bphcuda report) thrustのSConsビルドスクリプトを読み解く

bphcudaを復活させるに当たって障壁の1つはビルドである. thrustは自らのテストコードなどをビルドするためにSConsを使っている. bphcudaでも, 過去のビルドスクリプトをすべて捨て, thrustの設計に倣いたい設計を行おうと思う.

testingのビルドを倣いたいと思う. テストライブラリについては, thrustについているunittestなるものを使うか, 私が使っていたgtestを使うか迷いどころだが, とりあえずgtestを使おうと思う. まずは過去の実装をビルド出来るようにして, それから改善していく.

.
|-- SConscript #1
|-- SConstruct #2
|-- site_scons #3
|   `-- site_tools
|       |-- nvcc.py
|       `-- zip.py
|-- testing
|   |-- SConscript #4
|   |-- adjacent_difference.cu
|   |-- advance.cu
|   |-- allocator.cu
|   |-- backend
|   |   |-- SConscript #5
|   |   |-- cuda
|   |   |   |-- adjacent_difference.cu
|   |   |   |-- arch.cu
|   |   |   |-- block
|   |   |   |   `-- inclusive_scan.cu
|   |   |   |-- count.cu
|   |   |   |-- cudart.cu
|   |   |   |-- equal.cu
|   |   |   |-- fill.cu
|   |   |   |-- find.cu
|   |   |   |-- for_each.cu

SConsについて基本的なドキュメントは修士の時にざっと目を通した気がするが, thrustのビルドスクリプトはSConsの機能を使いこなしている(メタプログラミング含めて).

SConstrustというのが基本的なビルド情報を記述したファイルであろう.

def command_line_variables():
  # allow the user discretion to select the MSVC version
  vars = Variables()
  if os.name == 'nt':
    vars.Add(EnumVariable('MSVC_VERSION', 'MS Visual C++ version', None, allowed_values=('8.0', '9.0', '10.0')))

  # add a variable to handle the host backend
  vars.Add(ListVariable('host_backend', 'The host backend to target', 'cpp',
                        ['cpp', 'omp', 'tbb']))

  # add a variable to handle the device backend
  vars.Add(ListVariable('device_backend', 'The parallel device backend to target', 'cuda',
                        ['cuda', 'omp', 'tbb', 'cpp']))

  # add a variable to handle release/debug mode
  vars.Add(EnumVariable('mode', 'Release versus debug mode', 'release',
                        allowed_values = ('release', 'debug')))

  # add a variable to handle compute capability
  vars.Add(ListVariable('arch', 'Compute capability code generation', 'sm_10',
                        ['sm_10', 'sm_11', 'sm_12', 'sm_13', 'sm_20', 'sm_21', 'sm_30', 'sm_35']))

  # add a variable to handle warnings
  # only enable Wall by default on compilers other than cl
  vars.Add(BoolVariable('Wall', 'Enable all compilation warnings', os.name != 'nt'))

  # add a variable to treat warnings as errors
  vars.Add(BoolVariable('Werror', 'Treat warnings as errors', 0))

  return vars


# create a master Environment
vars = command_line_variables()

master_env = Environment(variables = vars, tools = ['default', 'nvcc', 'zip'])

envというオブジェクトが存在するらしい. 引数から推測するに, これはsconsコマンドを表現するオブジェクトではないか. nvccやらzipというのは, site_toolsに入っている. これは何かコマンドを追加しているような気がする. variablesというのは, コマンドに入力される直交した引数のことだろう.

for (host,device) in itertools.product(host_backends, device_backends):
  # clone the master environment for this config
  env = master_env.Clone()

  # populate the environment
  env.Append(CPPPATH = inc_paths(env, host, device))

  env.Append(CCFLAGS = cc_compiler_flags(env.subst('$CXX'), env['mode'], env['PLATFORM'], host, device, env['Wall'], env['Werror']))

  env.Append(NVCCFLAGS = nv_compiler_flags(env['mode'], device, env['arch']))

  env.Append(LINKFLAGS = linker_flags(env.subst('$LINK'), env['mode'], env['PLATFORM'], device))

  env.Append(LIBPATH = lib_paths(env, host, device))

  env.Append(LIBS = libs(env, env.subst('$CXX'), host, device))

  # assemble the name of this configuration's targets directory
  targets_dir = 'targets/{0}_host_{1}_device_{2}'.format(host, device, env['mode'])

  # allow subsidiary SConscripts to peek at the backends
  env['host_backend'] = host
  env['device_backend'] = device

  # invoke each SConscript with a variant directory
  env.SConscript('examples/SConscript',    exports='env', variant_dir = 'examples/'    + targets_dir, duplicate = 0)
  env.SConscript('testing/SConscript',     exports='env', variant_dir = 'testing/'     + targets_dir, duplicate = 0)
  env.SConscript('performance/SConscript', exports='env', variant_dir = 'performance/' + targets_dir, duplicate = 0)

env = master_env
master_env.SConscript('SConscript', exports='env', variant_dir = 'targets', duplicate = False)

引数のproduct全通りのビルドを行うと読める. variant_dirというのは, 生成物が格納されるディレクトリらしい. duplicateというのは, ソースコードをそのディレクトリにコピーしてから生成しますか?という意味らしい. すべて0に切ってあるのでたぶんおれも0にすべきなんだろう. この部分を書き換えることが必要だと思う.

Import('env')

# clone the parent's env so that we do not modify it
my_env = env.Clone()

vars = Variables()

# add a variable to filter source files by a regex
vars.Add('tests', 'Filter test files using a regex', '.')

# update variables
my_env.Help(vars.GenerateHelpText(env))
vars.Update(my_env)

# populate the environment

# with cl we have to do /bigobj
if my_env.subst('$CXX') == 'cl':
  my_env.Append(CPPFLAGS = '/bigobj')

# #include the current directory
my_env.Append(CPPPATH = Dir('.').srcnode())

# find all .cus & .cpps
sources = []
extensions  = ['*.cu', '*.cpp']

# gather sources in the current directorie
for ext in extensions:
  sources.extend(my_env.Glob(ext))

# gather sources from directories
sources.extend(SConscript('backend/SConscript', exports='env'))

# filter sources
import re
filter_exp = 'int main|TestDriver|{0}'.format(my_env['tests'])
pattern = re.compile(filter_exp)
def test_filter(src):
  return pattern.search(src.get_contents())

sources = filter(test_filter, sources)

tester = my_env.Program('tester', sources)

# create a 'unit_tests' alias
unit_tests_alias = my_env.Alias('unit_tests', [tester])

# add the verbose tester to the 'run_unit_tests' alias
run_unit_tests_alias = my_env.Alias('run_unit_tests', [tester], tester[0].abspath + ' --verbose')

# always build the 'run_unit_tests' target whether or not it needs it
my_env.AlwaysBuild(run_unit_tests_alias)

# add the unit tests alias to the 'run_tests' alias
my_env.Alias('run_tests', [tester], tester[0].abspath)

# build children
SConscript('trivial_tests/SConscript', exports='env')

Appendはパスや環境変数に追加していく意味だろうか. たぶん, テスト以下ではこれをつけるのが良いだろう.

(続く・・?)