Got test_parallel_vectorize working.

This commit is contained in:
Siu Kwan Lam 2012-08-06 11:35:03 -07:00
commit 1594e5456a
3 changed files with 253 additions and 142 deletions

View file

@ -5,6 +5,7 @@
import contextlib import contextlib
import llvm.core as lc import llvm.core as lc
import llvm.ee as le import llvm.ee as le
from llvm import LLVMException
def _is_int(ty): def _is_int(ty):
return isinstance(ty, lc.IntegerType) return isinstance(ty, lc.IntegerType)
@ -245,6 +246,44 @@ class CBuilder(object):
yield cb yield cb
cb.close() cb.close()
@contextlib.contextmanager
def forever(self):
with self.loop() as loop:
with loop.condition() as setcond:
NULL = self.constant_null(lc.Type.int())
setcond( NULL == NULL )
with loop.body():
yield loop
@contextlib.contextmanager
def for_range(self, *args):
def check_arg(x):
if isinstance(x, int):
return self.constant(lc.Type.int(), x)
if not x.is_int:
raise TypeError(x, "All args must be of integer type.")
return x
if len(args) == 3:
start, stop, step = map(check_arg, args)
elif len(args) == 2:
start, stop = map(check_arg, args)
step = self.constant(start.type, 1)
elif len(args) == 1:
stop = check_arg(args[0])
start = self.constant(stop.type, 0)
step = self.constant(stop.type, 1)
else:
raise TypeError("Invalid # of arguments: 1, 2 or 3")
idx = self.var_copy(start)
with self.loop() as loop:
with loop.condition() as setcond:
setcond( idx < stop )
with loop.body():
yield loop, idx
idx += step
def position_at_end(self, bb): def position_at_end(self, bb):
self.basic_block = bb self.basic_block = bb
self.builder.position_at_end(bb) self.builder.position_at_end(bb)
@ -270,13 +309,18 @@ class CBuilder(object):
def constant_string(self, string): def constant_string(self, string):
mod = self.function.module mod = self.function.module
name = '.conststr.%x' % hash(string) name = '.conststr.%x_%x' % (hash(string), len(string))
content = lc.Constant.stringz(string) content = lc.Constant.stringz(string)
globalstr = mod.add_global_variable(content.type, name=name) try:
globalstr.initializer = content globalstr = mod.get_global_variable_named(name)
ptr = mod.add_global_variable(lc.Type.pointer(content.type.element), except LLVMException:
name=name+".ptr") globalstr = mod.add_global_variable(content.type, name=name)
return CTemp(self, globalstr.bitcast(lc.Type.pointer(content.type.element))) globalstr.initializer = content
# ptr = mod.add_global_variable(lc.Type.pointer(content.type.element),
# name=name+".ptr")
return CTemp(self, globalstr.bitcast(
lc.Type.pointer(content.type.element)))
def get_intrinsic(self, intrinsic_id, tys): def get_intrinsic(self, intrinsic_id, tys):
lfunc = lc.Function.intrinsic(self.function.module, intrinsic_id, tys) lfunc = lc.Function.intrinsic(self.function.module, intrinsic_id, tys)

View file

@ -1,3 +1,15 @@
'''
This file implements the code-generator for parallel-vectorize.
ParallelUFunc is the platform independent base class for generating
the thread dispatcher. This thread dispatcher launches threads
that execute the generated function of UFuncCore.
UFuncCore is subclassed to specialize for the input/output types.
The actual workload is invoked inside the function generated by UFuncCore.
UFuncCore also defines a work-stealing mechanism that allows idle threads
to steal works from other threads.
'''
from llvm.core import * from llvm.core import *
from llvm.passes import * from llvm.passes import *
@ -5,14 +17,21 @@ from llvm_cbuilder import *
import llvm_cbuilder.shortnames as C import llvm_cbuilder.shortnames as C
class WorkQueue(CStruct): class WorkQueue(CStruct):
'''
Structure for workqueue for parallel-ufunc.
'''
_fields_ = [ _fields_ = [
('next', C.intp), ('next', C.intp), # next index of work item
('last', C.intp), ('last', C.intp), # last index of work item (exlusive)
('lock', C.int), ('lock', C.int), # for locking the workqueue
] ]
def Lock(self): def Lock(self):
'''
Inline the lock procedure.
'''
with self.parent.loop() as loop: with self.parent.loop() as loop:
with loop.condition() as setcond: with loop.condition() as setcond:
unlocked = self.parent.constant(self.lock.type, 0) unlocked = self.parent.constant(self.lock.type, 0)
@ -26,6 +45,9 @@ class WorkQueue(CStruct):
pass pass
def Unlock(self): def Unlock(self):
'''
Inline the unlock procedure.
'''
unlocked = self.parent.constant(self.lock.type, 0) unlocked = self.parent.constant(self.lock.type, 0)
locked = self.parent.constant(self.lock.type, 1) locked = self.parent.constant(self.lock.type, 1)
@ -39,6 +61,9 @@ class WorkQueue(CStruct):
class ContextCommon(CStruct): class ContextCommon(CStruct):
'''
Structure for thread-shared context information in parallel-ufunc.
'''
_fields_ = [ _fields_ = [
# loop ufunc args # loop ufunc args
('args', C.pointer(C.char_p)), ('args', C.pointer(C.char_p)),
@ -52,6 +77,9 @@ class ContextCommon(CStruct):
] ]
class Context(CStruct): class Context(CStruct):
'''
Structure for thread-specific context information in parallel-ufunc.
'''
_fields_ = [ _fields_ = [
('common', C.pointer(ContextCommon.llvm_type())), ('common', C.pointer(ContextCommon.llvm_type())),
('id', C.int), ('id', C.int),
@ -71,14 +99,14 @@ class ParallelUFunc(CDefinition):
] ]
def body(self, func, worker, args, dimensions, steps, data, ThreadCount=1): def body(self, func, worker, args, dimensions, steps, data, ThreadCount=1):
# Setup variables
common = self.var(ContextCommon, name='common') common = self.var(ContextCommon, name='common')
workqueues = self.array(WorkQueue, ThreadCount, name='workqueues') workqueues = self.array(WorkQueue, ThreadCount, name='workqueues')
contexts = self.array(Context, ThreadCount, name='contexts') contexts = self.array(Context, ThreadCount, name='contexts')
num_thread = self.var(C.int, ThreadCount, name='num_thread') num_thread = self.var(C.int, ThreadCount, name='num_thread')
# Initialize ContextCommon
common.args.assign(args) common.args.assign(args)
common.dimensions.assign(dimensions) common.dimensions.assign(dimensions)
common.steps.assign(steps) common.steps.assign(steps)
@ -87,6 +115,11 @@ class ParallelUFunc(CDefinition):
common.num_thread.assign(num_thread.cast(C.int)) common.num_thread.assign(num_thread.cast(C.int))
common.workqueues.assign(workqueues.reference()) common.workqueues.assign(workqueues.reference())
# Determine chunksize, initial count of work-items per thread.
# If total_work >= num_thread, equally divide the works.
# If total_work % num_thread != 0, the last thread does all remaining works.
# If total_work < num_thread, each thread does one work,
# and set num_thread to total_work
N = dimensions[0] N = dimensions[0]
ChunkSize = self.var_copy(N / num_thread.cast(N.type)) ChunkSize = self.var_copy(N / num_thread.cast(N.type))
ChunkSize_NULL = self.constant_null(ChunkSize.type) ChunkSize_NULL = self.constant_null(ChunkSize.type)
@ -95,60 +128,70 @@ class ParallelUFunc(CDefinition):
ChunkSize.assign(self.constant(ChunkSize.type, 1)) ChunkSize.assign(self.constant(ChunkSize.type, 1))
num_thread.assign(N.cast(num_thread.type)) num_thread.assign(N.cast(num_thread.type))
# Populate workqueue for all threads
self._populate_workqueues(workqueues, N, ChunkSize, num_thread) self._populate_workqueues(workqueues, N, ChunkSize, num_thread)
# Populate contexts for all threads
self._populate_context(contexts, common, num_thread) self._populate_context(contexts, common, num_thread)
# Dispatch worker threads
self._dispatch_worker(worker, contexts, num_thread) self._dispatch_worker(worker, contexts, num_thread)
## DEBUG ONLY ##
# Check for race condition # Check for race condition
total_completed = self.var(C.intp, 0, name='total_completed') if True:
for t in range(ThreadCount): total_completed = self.var(C.intp, 0, name='total_completed')
cur_ctxt = contexts[t].as_struct(Context) for t in range(ThreadCount):
total_completed += cur_ctxt.completed cur_ctxt = contexts[t].as_struct(Context)
self.debug(cur_ctxt.id, 'completed', cur_ctxt.completed) total_completed += cur_ctxt.completed
self.debug(cur_ctxt.id, 'completed', cur_ctxt.completed)
with self.ifelse( total_completed == N ) as ifelse: with self.ifelse( total_completed == N ) as ifelse:
with ifelse.then(): with ifelse.then():
self.debug("All is well!") self.debug("All is well!")
with ifelse.otherwise(): with ifelse.otherwise():
self.debug("ERROR: race occurred!") self.debug("ERROR: race occurred! Trigger segfault")
self.unreachable()
# Return
self.ret() self.ret()
def _populate_workqueues(self, workqueues, N, ChunkSize, num_thread): def _populate_workqueues(self, workqueues, N, ChunkSize, num_thread):
i = self.var(num_thread.type, 0, name='i') '''
ONE = self.constant(i.type, 1) Loop over all threads and populate the workqueue for each of them.
with self.loop() as loop: '''
with loop.condition() as setcond: ONE = self.constant(num_thread.type, 1)
setcond( i < num_thread ) with self.for_range(num_thread) as (loop, i):
with loop.body(): cur_wq = workqueues[i].as_struct(WorkQueue)
cur_wq = workqueues[i].as_struct(WorkQueue) cur_wq.next.assign(i.cast(ChunkSize.type) * ChunkSize)
cur_wq.next.assign(i.cast(ChunkSize.type) * ChunkSize) cur_wq.last.assign((i + ONE).cast(ChunkSize.type) * ChunkSize)
cur_wq.last.assign((i + ONE).cast(ChunkSize.type) * ChunkSize) cur_wq.lock.assign(self.constant(C.int, 0))
cur_wq.lock.assign(self.constant(C.int, 0))
# increment
i += ONE
# end loop # end loop
last_wq = workqueues[num_thread - ONE].as_struct(WorkQueue) last_wq = workqueues[num_thread - ONE].as_struct(WorkQueue)
last_wq.last.assign(N) last_wq.last.assign(N)
def _populate_context(self, contexts, common, num_thread): def _populate_context(self, contexts, common, num_thread):
i = self.var(num_thread.type, 0, name='i') '''
ONE = self.constant(i.type, 1) Loop over all threads and populate contexts for each of them.
with self.loop() as loop: '''
with loop.condition() as setcond: ONE = self.constant(num_thread.type, 1)
setcond( i < num_thread ) with self.for_range(num_thread) as (loop, i):
with loop.body(): cur_ctxt = contexts[i].as_struct(Context)
cur_ctxt = contexts[i].as_struct(Context) cur_ctxt.common.assign(common.reference())
cur_ctxt.common.assign(common.reference()) cur_ctxt.id.assign(i)
cur_ctxt.id.assign(i) cur_ctxt.completed.assign(
cur_ctxt.completed.assign(self.constant_null( self.constant_null(cur_ctxt.completed.type))
cur_ctxt.completed.type))
# increment
i += ONE
def _dispatch_worker(self, worker, contexts, num_thread):
'''
Dispatch worker threads.
'''
raise NotImplementedError
class ParallelUFuncPosix(ParallelUFunc): class ParallelUFuncPosix(ParallelUFunc):
'''
Implements _dispatch_worker to use pthread.
'''
def _dispatch_worker(self, worker, contexts, num_thread): def _dispatch_worker(self, worker, contexts, num_thread):
api = PThreadAPI(self) api = PThreadAPI(self)
NULL = self.constant_null(C.void_p) NULL = self.constant_null(C.void_p)
@ -158,27 +201,19 @@ class ParallelUFuncPosix(ParallelUFunc):
# self.debug("launch threads") # self.debug("launch threads")
# TODO error handling # TODO error handling
i = self.var(num_thread.type, 0, name='i') ONE = self.constant(num_thread.type, 1)
ONE = self.constant(i.type, 1) with self.for_range(num_thread) as (loop, i):
with self.loop() as loop: api.pthread_create(threads[i].reference(), NULL, worker,
with loop.condition() as setcond: contexts[i].reference().cast(C.void_p))
setcond( i < num_thread )
with loop.body():
api.pthread_create(threads[i].reference(), NULL, worker,
contexts[i].reference().cast(C.void_p))
i += ONE
# self.debug("join threads") with self.for_range(num_thread) as (loop, i):
i.assign(self.constant_null(i.type)) api.pthread_join(threads[i], NULL)
with self.loop() as loop:
with loop.condition() as setcond:
setcond( i < num_thread )
with loop.body():
api.pthread_join(threads[i], NULL)
i += ONE
# self.debug("closing")
class UFuncCore(CDefinition): class UFuncCore(CDefinition):
'''
Generates the workqueue handling and work stealing and invoke
the work function for each work item.
'''
_name_ = 'ufunc_worker' _name_ = 'ufunc_worker'
_argtys_ = [ _argtys_ = [
('context', C.pointer(Context.llvm_type())), ('context', C.pointer(Context.llvm_type())),
@ -203,64 +238,61 @@ class UFuncCore(CDefinition):
''' '''
ZERO = self.constant_null(C.int) ZERO = self.constant_null(C.int)
with self.loop() as loop: with self.forever() as loop:
with loop.condition() as setcond: workqueue.Lock()
setcond(ZERO == ZERO) # loop forever # Critical section
item = self.var_copy(workqueue.next, name='item')
workqueue.next += self.constant(item.type, 1)
last = self.var_copy(workqueue.last, name='last')
# Release
workqueue.Unlock()
with loop.body(): with self.ifelse( item >= last ) as ifelse:
workqueue.Lock() with ifelse.then():
# Critical section loop.break_loop()
item = self.var_copy(workqueue.next, name='item')
workqueue.next += self.constant(item.type, 1)
last = self.var_copy(workqueue.last, name='last')
# Release
workqueue.Unlock()
with self.ifelse( item >= last ) as ifelse: self._do_work(common, item, tid)
with ifelse.then(): completed += self.constant(completed.type, 1)
loop.break_loop()
self._do_work(common, item, tid, completed)
def _do_work_stealing(self, common, tid, completed): def _do_work_stealing(self, common, tid, completed):
''' '''
Steal work from other workqueues Steal work from other workqueues.
''' '''
# self.debug("start work stealing", tid) # self.debug("start work stealing", tid)
incomplete_thread_ct = self.var(C.int, 1) steal_continue = self.var(C.int, 1)
ITC_NULL = self.constant_null(incomplete_thread_ct.type) STEAL_STOP = self.constant_null(steal_continue.type)
# Loop until all workqueues are done.
with self.loop() as loop: with self.loop() as loop:
with loop.condition() as setcond: with loop.condition() as setcond:
setcond( incomplete_thread_ct != ITC_NULL ) setcond( steal_continue != STEAL_STOP )
with loop.body(): with loop.body():
incomplete_thread_ct.assign(ITC_NULL) steal_continue.assign(STEAL_STOP)
self._do_work_stealing_innerloop(common, incomplete_thread_ct, self._do_work_stealing_innerloop(common, steal_continue, tid,
tid, completed) completed)
def _do_work_stealing_innerloop(self, common, incomplete_thread_ct, tid, def _do_work_stealing_innerloop(self, common, steal_continue, tid,
completed): completed):
i = self.var(C.int, 0) '''
with self.loop() as loop: Loop over all other threads and try to steal work.
with loop.condition() as setcond: '''
setcond( i < common.num_thread ) with self.for_range(common.num_thread) as (loop, i):
with self.ifelse( i != tid ) as ifelse:
with ifelse.then():
otherqueue = common.workqueues[i].as_struct(WorkQueue)
self._do_work_stealing_check(common, otherqueue,
steal_continue, tid,
completed)
with loop.body(): def _do_work_stealing_check(self, common, otherqueue, steal_continue, tid,
with self.ifelse( i != tid ) as ifelse: completed):
with ifelse.then(): '''
otherqueue = common.workqueues[i].as_struct(WorkQueue) Check the workqueue for any remaining work and steal it.
self._do_work_stealing_check(common, otherqueue, '''
incomplete_thread_ct, tid,
completed)
# increment
i += self.constant(i.type, 1)
def _do_work_stealing_check(self, common, otherqueue,
incomplete_thread_ct, tid, completed):
otherqueue.Lock() otherqueue.Lock()
ONE = self.constant(otherqueue.last.type, 1) ONE = self.constant(otherqueue.last.type, 1)
ITC_ONE = self.constant(incomplete_thread_ct.type, 1) STEAL_CONTINUE = self.constant(steal_continue.type, 1)
with self.ifelse(otherqueue.next < otherqueue.last) as ifelse: with self.ifelse(otherqueue.next < otherqueue.last) as ifelse:
with ifelse.then(): with ifelse.then():
otherqueue.last -= ONE otherqueue.last -= ONE
@ -269,17 +301,20 @@ class UFuncCore(CDefinition):
otherqueue.Unlock() otherqueue.Unlock()
# Released # Released
self._do_work(common, item, tid, completed) self._do_work(common, item, tid)
completed += self.constant(completed.type, 1)
# Mark incomplete thread # Mark incomplete thread
incomplete_thread_ct.assign(ITC_ONE) steal_continue.assign(STEAL_CONTINUE)
with ifelse.otherwise(): with ifelse.otherwise():
otherqueue.Unlock() otherqueue.Unlock()
def _do_work(self, common, item, tid, completed): def _do_work(self, common, item, tid):
# self.debug(" tid", tid, "item", item) '''
completed += self.constant(completed.type, 1) Prepare to call the actual work function.
'''
raise NotImplementedError
class PThreadAPI(CExternal): class PThreadAPI(CExternal):
@ -294,25 +329,3 @@ class PThreadAPI(CExternal):
pthread_join = Type.function(C.int, [C.void_p, C.void_p]) pthread_join = Type.function(C.int, [C.void_p, C.void_p])
def _main():
NUM_THREAD = 2
module = Module.new(__name__)
fpm = FunctionPassManager.new(module)
PassManagerBuilder.new().populate(fpm)
f1 = ParallelUFuncPosix.define(module, ThreadCount=NUM_THREAD)
f2 = UFuncCore.define(module)
#print(module)
# print(f1)
print(f2)
module.verify()
fpm.run(f1)
fpm.run(f2)
print('optimized'.center(80,'-'))
print(f2)
if __name__ == '__main__':
_main()

View file

@ -1,11 +1,40 @@
from parallel_vectorize import * from parallel_vectorize import *
'''
void parallel_ufunc(void * func, void * worker, class Work_D_D(CDefinition):
char **args, npy_intp *dimensions, _name_ = 'copy_d_d'
npy_intp *steps, void *data) _retty_ = C.double
''' _argtys_ = [
('inval', C.double),
]
def body(self, inval):
self.ret(inval / self.constant(inval.type, 2.345))
class UFuncCore_D_D(UFuncCore):
'''
Specialize UFuncCore for double input, double output.
'''
def _do_work(self, common, item, tid):
ufunc_type = Type.function(C.double, [C.double])
ufunc_ptr = CFunc(self, common.func.cast(C.pointer(ufunc_type)).value)
inbase = common.args[0]
outbase = common.args[1]
instep = common.steps[0]
outstep = common.steps[0]
indata = inbase[item * instep].reference().cast(C.pointer(C.double))
outdata = outbase[item * outstep].reference().cast(C.pointer(C.double))
res = ufunc_ptr(indata.load())
outdata.store(res)
class Tester(CDefinition): class Tester(CDefinition):
'''
Generate test.
'''
_name_ = 'tester' _name_ = 'tester'
def body(self): def body(self):
@ -14,20 +43,23 @@ class Tester(CDefinition):
ThreadCount = 2 ThreadCount = 2
ArgCount = 2 ArgCount = 2
WorkCount = 1000000 WorkCount = 10000
parallel_ufunc = CFunc(self, ParallelUFuncPosix.define(module, parallel_ufunc = CFunc(self, ParallelUFuncPosix.define(module,
ThreadCount=ThreadCount)) ThreadCount=ThreadCount))
worker = CFunc(self, UFuncCore.define(module)) ufunc_core = CFunc(self, UFuncCore_D_D.define(module))
worker = CFunc(self, Work_D_D.define(module))
# real work # real work
NULL = self.constant_null(C.void_p) NULL = self.constant_null(C.void_p)
args = self.array(C.char_p, 2, name='args') args = self.array(C.char_p, 2, name='args')
args_double = []
for t in range(ThreadCount): for t in range(ThreadCount):
args_for_thread = self.array(C.double, 4) args_for_thread = self.array(C.double, WorkCount)
args[t].assign(args_for_thread.cast(C.char_p)) args[t].assign(args_for_thread.cast(C.char_p))
args_double.append(args_for_thread)
dims = self.array(C.intp, 1, name='dims') dims = self.array(C.intp, 1, name='dims')
dims[0].assign(self.constant(C.intp, WorkCount)) dims[0].assign(self.constant(C.intp, WorkCount))
@ -37,7 +69,28 @@ class Tester(CDefinition):
for c in range(ArgCount): for c in range(ArgCount):
steps[c].assign(self.constant(C.intp, 8)) steps[c].assign(self.constant(C.intp, 8))
parallel_ufunc(NULL, worker.cast(C.void_p), args, dims, steps, NULL) # populate data
inbase = args_double[0]
i = self.var(C.intp, 0)
with self.loop() as loop:
with loop.condition() as setcond:
setcond( i < self.constant(C.intp, WorkCount) )
with loop.body():
inbase[i].assign(i.cast(C.double))
i += self.constant(C.intp, 1)
# call parallel ufunc
parallel_ufunc(worker.cast(C.void_p), ufunc_core.cast(C.void_p), args,
dims, steps, NULL)
# check error
outbase = args_double[-1]
with self.for_range(self.constant(C.intp, WorkCount)) as (loop, i):
test = outbase[i] != (inbase[i] / self.constant(C.double, 2.345))
with self.ifelse( test ) as ifelse:
with ifelse.then():
self.debug("Invalid data at i =", i, outbase[i], inbase[i])
self.ret() self.ret()
@ -61,6 +114,7 @@ def main():
print(module) print(module)
# run # run
print('run')
exe = CExecutor(module) exe = CExecutor(module)
exe.engine.get_pointer_to_function(fntester) exe.engine.get_pointer_to_function(fntester)
func = exe.get_ctype_function(fntester, 'void') func = exe.get_ctype_function(fntester, 'void')