diff --git a/devito/ir/iet/nodes.py b/devito/ir/iet/nodes.py index 554507230f..e15a237c67 100644 --- a/devito/ir/iet/nodes.py +++ b/devito/ir/iet/nodes.py @@ -18,7 +18,7 @@ AFFINE, INBOUND, PARALLEL, PARALLEL_IF_ATOMIC, PARALLEL_IF_PVT, SEQUENTIAL, VECTORIZED, Forward, PrefetchUpdate, Property, WithLock ) -from devito.symbolics import CallFromPointer, ListInitializer, Macro +from devito.symbolics import Byref, CallFromPointer, DefFunction, ListInitializer, Macro from devito.tools import ( Signer, as_tuple, ctypes_to_cstr, filter_ordered, filter_sorted, flatten ) @@ -62,12 +62,13 @@ 'Section', 'Switch', 'SyncSpot', - 'ThreadFence', 'TimedList', 'Transfer', 'Using', 'UsingNamespace', 'While', + 'atomic_load', + 'atomic_store', ] # First-class IET nodes @@ -1304,29 +1305,24 @@ def size(self): return self.ispace.size -class ThreadFence(Call): - +def atomic_load(flag): """ - A memory fence, ordering a thread's accesses either side of it. - - Threads exchanging work through a shared flag need memory ordering on both - sides: a release before publishing the flag, and an acquire after observing it. - This ensures the state associated with the flag is visible before it is used. - Without this ordering, updates may be observed in the wrong order and lost. + An acquire load of a flag that threads synchronise on, for use in a wait + condition. Reads that follow it cannot be satisfied before it. - `__atomic_thread_fence` is a compiler builtin, so this renders the same in C - and in C++ and needs no header. + The `__atomic` builtins accept the existing `volatile int` flags and are + valid in C and C++ alike, so no declaration changes and no header is needed. """ + return DefFunction('__atomic_load_n', (Byref(flag), Macro('__ATOMIC_ACQUIRE'))) - def __init__(self, order): - assert order in ('acquire', 'release') - super().__init__('__atomic_thread_fence', - [Macro(f'__ATOMIC_{order.upper()}')]) - self._order = order - @property - def order(self): - return self._order +def atomic_store(flag, value): + """ + A release store to a flag that threads synchronise on, the counterpart of + `atomic_load`: everything sequenced before it is visible to whoever then + acquires `flag`. + """ + return Call('__atomic_store_n', (Byref(flag), value, Macro('__ATOMIC_RELEASE'))) class Prodder(Call): diff --git a/devito/passes/iet/asynchrony.py b/devito/passes/iet/asynchrony.py index bf51678008..7ef86acc55 100644 --- a/devito/passes/iet/asynchrony.py +++ b/devito/passes/iet/asynchrony.py @@ -7,8 +7,8 @@ from devito.ir import ( AsyncCall, AsyncCallable, BlankLine, BusyWait, BusyWaitCall, Call, Callable, Conditional, DummyEq, DummyExpr, EntryFunction, FindNodes, FindSymbols, Increment, - Iteration, List, PointerCast, Return, ThreadCallable, ThreadFence, Transformer, While, - make_callable, maybe_alias + Iteration, List, PointerCast, Return, ThreadCallable, Transformer, While, atomic_load, + atomic_store, make_callable, maybe_alias ) from devito.passes.iet.definitions import DataManager from devito.passes.iet.engine import iet_pass @@ -70,11 +70,13 @@ def _lower_async_objs(iet, tracker=None, sregistry=None, **kwargs): if sdata.size == 1: d = sdata.index - condition = CondNe(FieldFromComposite(sdata.symbolic_flag, sdata[d]), 1) + flag = FieldFromComposite(sdata.symbolic_flag, sdata[d]) + condition = CondNe(atomic_load(flag), 1) activation = [BusyWait(condition)] else: d = Temp(name=sregistry.make_name(prefix=sdata.index.name)) - condition = CondNe(FieldFromComposite(sdata.symbolic_flag, sdata[d]), 1) + flag = FieldFromComposite(sdata.symbolic_flag, sdata[d]) + condition = CondNe(atomic_load(flag), 1) activation = [DummyExpr(d, 0), BusyWait(condition, DummyExpr(d, (d + 1) % sdata.size))] arguments = [] @@ -88,13 +90,8 @@ def _lower_async_objs(iet, tracker=None, sregistry=None, **kwargs): activation.extend([DummyExpr(FieldFromComposite(i.base, sdata[d]), i) for i in arguments]) - # Publishing the request must happen last. Everything the thread depends on - # must be visible before this flag is set. `volatile` does not provide that - # ordering, so weakly ordered targets could otherwise observe stale state. - activation.append(ThreadFence('release')) - activation.append( - DummyExpr(FieldFromComposite(sdata.symbolic_flag, sdata[d]), 2) - ) + # The request is published last, so the thread sees everything set above + activation.append(atomic_store(flag, 2)) name = sregistry.make_name(prefix='activate') efunc = make_callable(name, activation) @@ -134,6 +131,7 @@ def _(iet, key=None, tracker=None, sregistry=None, **kwargs): pname=sregistry.make_name(prefix='tsdata') ) sbase = sdata.indexed + flag = FieldFromPointer(sdata.symbolic_flag, sbase) # PThreadArray -- that is he pthreads that will execute the AsyncCallable # asynchronously @@ -144,31 +142,25 @@ def _(iet, key=None, tracker=None, sregistry=None, **kwargs): tbase = threads.indexed # Prepend the SharedData fields available upon thread activation - preactions = [ThreadFence('acquire')] - preactions.extend(DummyExpr(i, FieldFromPointer(i.base, sbase)) - for i in ncfields) + preactions = [DummyExpr(i, FieldFromPointer(i.base, sbase)) for i in ncfields] preactions.append(BlankLine) # Append the flag reset postactions = [List(body=[ BlankLine, # Whatever the task produced -- data in a buffer, a lock handed back -- - # has to be visible before the flag says it is done. - ThreadFence('release'), - DummyExpr(FieldFromPointer(sdata.symbolic_flag, sbase), 1) + # has to be visible before the flag says it is done + atomic_store(flag, 1) ])] wrap = List(body=preactions + list(iet.body.body) + postactions) # The thread has work to do when it receives the signal that all locks have # been set to 0 by the main thread - wrap = Conditional( - CondEq(FieldFromPointer(sdata.symbolic_flag, sbase), 2), - wrap - ) + wrap = Conditional(CondEq(atomic_load(flag), 2), wrap) # The thread keeps spinning until the alive flag is set to 0 by the main thread - wrap = While(CondNe(FieldFromPointer(sdata.symbolic_flag, sbase), 0), wrap) + wrap = While(CondNe(atomic_load(flag), 0), wrap) # pthread functions expect exactly one argument of type void* tparameter = Pointer(name=f'_{sdata.name}') @@ -208,7 +200,7 @@ def _(iet, key=None, tracker=None, sregistry=None, **kwargs): body.append(DummyExpr(FieldFromPointer(i.base, sbase), i.base)) body.extend([ BlankLine, - DummyExpr(FieldFromPointer(sdata.symbolic_flag, sbase), 1) + DummyExpr(flag, 1) ]) body.extend([ BlankLine, @@ -223,8 +215,8 @@ def _(iet, key=None, tracker=None, sregistry=None, **kwargs): # Create an efunc to shutdown the pthreads name = sregistry.make_name(prefix='shutdown') body = List(body=callback([ - BusyWait(CondEq(FieldFromPointer(sdata.symbolic_flag, sbase), 2)), - DummyExpr(FieldFromPointer(sdata.symbolic_flag, sbase), 0), + BusyWait(CondEq(atomic_load(flag), 2)), + atomic_store(flag, 0), Call('pthread_join', (threads[d], Null)) ])) shutdown = make_callable(name, body) diff --git a/devito/passes/iet/orchestration.py b/devito/passes/iet/orchestration.py index d1596d8881..ef2c7f5fec 100644 --- a/devito/passes/iet/orchestration.py +++ b/devito/passes/iet/orchestration.py @@ -7,8 +7,8 @@ from devito.exceptions import CompilationError from devito.ir.iet import ( AsyncCall, AsyncCallable, BlankLine, Block, BusyWait, BusyWaitCall, Call, Callable, - Conditional, DummyExpr, List, SyncSpot, ThreadFence, Transformer, derive_parameters, - make_callable + Conditional, List, SyncSpot, Transformer, atomic_load, atomic_store, + derive_parameters, make_callable ) from devito.ir.iet.visitors import Visitor from devito.ir.support import ( @@ -44,7 +44,7 @@ def __init__(self, sregistry=None, options=None, **kwargs): def _make_waitlock(self, iet, sync_ops, *args): waitloop = List( - body=BusyWait(Or(*[CondEq(s.handle, 0) for s in sync_ops])), + body=BusyWait(Or(*[CondEq(atomic_load(s.handle), 0) for s in sync_ops])), ) iet = List(body=(waitloop,) + iet.body) @@ -53,12 +53,8 @@ def _make_waitlock(self, iet, sync_ops, *args): def _make_releaselock(self, iet, sync_ops, *args): pre = [] - pre.append(BusyWait(Or(*[CondNe(s.handle, 2) for s in sync_ops]))) - pre.append(ThreadFence('acquire')) - pre.extend(DummyExpr(s.handle, 0) for s in sync_ops) - # Hand the lock back before anything that follows can be observed -- - # in particular before the request that asks the thread to refill it. - pre.append(ThreadFence('release')) + pre.append(BusyWait(Or(*[CondNe(atomic_load(s.handle), 2) for s in sync_ops]))) + pre.extend(atomic_store(s.handle, 0) for s in sync_ops) name = self.sregistry.make_name(prefix="release_lock") parameters = derive_parameters(pre, ordering='canonical') @@ -421,7 +417,7 @@ def _(layer, iet, sync_ops, lang, sregistry): if lang._map_wait is not None: body.append(lang._map_wait(qid)) - body.extend([DummyExpr(s.handle, 1) for s in sync_ops]) + body.extend([atomic_store(s.handle, 1) for s in sync_ops]) body.append(BlankLine) name = f'copy_to_{layer.suffix}' @@ -433,7 +429,7 @@ def _(layer, iet, sync_ops, lang, sregistry): body.extend(list(iet.body)) body.append(BlankLine) - body.extend([DummyExpr(s.handle, 2) for s in sync_ops]) + body.extend([atomic_store(s.handle, 2) for s in sync_ops]) return body, name @@ -460,7 +456,7 @@ def _(layer, iet, sync_ops, lang, sregistry): body = [] name = f'prefetch_to_{layer.suffix}' - body.extend([DummyExpr(s.handle, 2) for s in sync_ops]) + body.extend([atomic_store(s.handle, 2) for s in sync_ops]) body = iet.body + (BlankLine,) + tuple(body) diff --git a/tests/test_gpu_common.py b/tests/test_gpu_common.py index 3efda2820f..637468eede 100644 --- a/tests/test_gpu_common.py +++ b/tests/test_gpu_common.py @@ -14,7 +14,8 @@ from devito.arch import Cpu64, Device, get_cpu_info, get_gpu_info from devito.exceptions import InvalidArgument from devito.ir import ( - Conditional, Expression, FindNodes, FindSymbols, Section, retrieve_iteration_tree + Call, Conditional, Expression, FindNodes, FindSymbols, Section, + retrieve_iteration_tree ) from devito.passes.iet.languages.openmp import OmpIteration from devito.types import DeviceID, DeviceRM, Lock, NPThreads, PThreadArray, Symbol @@ -410,19 +411,18 @@ def test_tasking_in_isolation(self, opt): assert len([i for i in FindSymbols().visit(op) if isinstance(i, Lock)]) == 1 sections = FindNodes(Section).visit(op) assert len(sections) == 3 - assert str(sections[0].body[0].body[0].body[0].body[0]) == 'while(lock0[0] == 0);' + assert str(sections[0].body[0].body[0].body[0].body[0]) ==\ + 'while(__atomic_load_n(&lock0[0],__ATOMIC_ACQUIRE) == 0);' body = op._func_table['release_lock0'].root.body - assert str(body.body[0].condition) == 'Ne(lock0[0], 2)' - # An acquire fence pairs with the thread's release before the lock is - # read back, and a release fence publishes it before the next request - assert 'atomic_thread_fence' in str(body.body[1]) - assert str(body.body[2]) == 'lock0[0] = 0;' - assert 'atomic_thread_fence' in str(body.body[3]) + assert str(body.body[0].condition) == \ + 'Ne(__atomic_load_n(&(lock0[0]), __ATOMIC_ACQUIRE), 2)' + assert str(body.body[1]) == '__atomic_store_n(&lock0[0],0,__ATOMIC_RELEASE);' body = op._func_table['activate0'].root.body - assert str(body.body[0].condition) == 'Ne(sdata0[0].flag, 1)' + assert str(body.body[0].condition) == \ + 'Ne(__atomic_load_n(&(sdata0[0].flag), __ATOMIC_ACQUIRE), 1)' assert str(body.body[1]) == 'sdata0[0].time = time;' - assert 'atomic_thread_fence' in str(body.body[2]) - assert str(body.body[3]) == 'sdata0[0].flag = 2;' + assert str(body.body[2]) == \ + '__atomic_store_n(&sdata0[0].flag,2,__ATOMIC_RELEASE);' op.apply(time_M=nt-2) @@ -454,11 +454,15 @@ def test_tasking_fused(self): b = 17 if configuration['language'] == 'openacc' else 16 # No `qid` w/ OMP assert str(exprs[b]) == 'const int deviceid = sdata0->deviceid;' assert str(exprs[b+1]) == 'volatile int time = sdata0->time;' - assert str(exprs[b+2]) == 'lock0[0] = 1;' - assert exprs[b+3].write is u - assert exprs[b+4].write is v - assert str(exprs[b+5]) == 'lock0[0] = 2;' - assert str(exprs[b+6]) == 'sdata0->flag = 1;' + assert exprs[b+2].write is u + assert exprs[b+3].write is v + # The lock and flag stores are release stores, not plain expressions + calls = FindNodes(Call).visit(op._func_table['copy_to_host0'].root) + assert [str(i) for i in calls if i.name == '__atomic_store_n'] == [ + '__atomic_store_n(&lock0[0],1,__ATOMIC_RELEASE);', + '__atomic_store_n(&lock0[0],2,__ATOMIC_RELEASE);', + '__atomic_store_n(&sdata0->flag,1,__ATOMIC_RELEASE);' + ] op.apply(time_M=nt-2) @@ -491,14 +495,15 @@ def test_tasking_unfused_two_locks(self): sections = FindNodes(Section).visit(op) assert len(sections) == 4 assert (str(sections[1].body[0].body[0].body[0].body[0]) == - 'while(lock0[0] == 0 || lock1[0] == 0);') # Wait-lock + 'while(__atomic_load_n(&lock0[0],__ATOMIC_ACQUIRE) == 0 || ' + '__atomic_load_n(&lock1[0],__ATOMIC_ACQUIRE) == 0);') # Wait-lock body = sections[2].body[0].body[0] assert str(body.body[0]) == 'release_lock0(lock0);' assert str(body.body[1]) == 'activate0(time,sdata0);' assert len(op._func_table) == 5 - exprs = FindNodes(Expression).visit(op._func_table['copy_to_host0'].root) - b = 18 if configuration['language'] == 'openacc' else 17 # No `qid` w/ OMP - assert str(exprs[b]) == 'lock0[0] = 1;' + calls = FindNodes(Call).visit(op._func_table['copy_to_host0'].root) + stores = [str(i) for i in calls if i.name == '__atomic_store_n'] + assert stores[0] == '__atomic_store_n(&lock0[0],1,__ATOMIC_RELEASE);' op.apply(time_M=nt-2) @@ -531,25 +536,30 @@ def test_tasking_forcefuse(self): sections = FindNodes(Section).visit(op) assert len(sections) == 3 assert (str(sections[1].body[0].body[0].body[0].body[0]) == - 'while(lock0[0] == 0 || lock1[0] == 0);') # Wait-lock + 'while(__atomic_load_n(&lock0[0],__ATOMIC_ACQUIRE) == 0 || ' + '__atomic_load_n(&lock1[0],__ATOMIC_ACQUIRE) == 0);') # Wait-lock body = op._func_table['release_lock0'].root.body - assert str(body.body[0].condition) == 'Ne(lock0[0], 2) | Ne(lock1[0], 2)' - assert 'atomic_thread_fence' in str(body.body[1]) - assert str(body.body[2]) == 'lock0[0] = 0;' # Set-lock - assert str(body.body[3]) == 'lock1[0] = 0;' # Set-lock - assert 'atomic_thread_fence' in str(body.body[4]) + assert str(body.body[0].condition) == \ + 'Ne(__atomic_load_n(&(lock0[0]), __ATOMIC_ACQUIRE), 2) | '\ + 'Ne(__atomic_load_n(&(lock1[0]), __ATOMIC_ACQUIRE), 2)' + # Set-lock + assert str(body.body[1]) == '__atomic_store_n(&lock0[0],0,__ATOMIC_RELEASE);' + assert str(body.body[2]) == '__atomic_store_n(&lock1[0],0,__ATOMIC_RELEASE);' body = op._func_table['activate0'].root.body - assert str(body.body[0].condition) == 'Ne(sdata0[0].flag, 1)' # Wait-thread + assert str(body.body[0].condition) == \ + 'Ne(__atomic_load_n(&(sdata0[0].flag), __ATOMIC_ACQUIRE), 1)' # Wait-thread assert str(body.body[1]) == 'sdata0[0].time = time;' - assert 'atomic_thread_fence' in str(body.body[2]) - assert str(body.body[3]) == 'sdata0[0].flag = 2;' + assert str(body.body[2]) == \ + '__atomic_store_n(&sdata0[0].flag,2,__ATOMIC_RELEASE);' assert len(op._func_table) == 5 exprs = FindNodes(Expression).visit(op._func_table['copy_to_host0'].root) b = 21 if configuration['language'] == 'openacc' else 20 # No `qid` w/ OMP - assert str(exprs[b]) == 'lock0[0] = 1;' - assert str(exprs[b+1]) == 'lock1[0] = 1;' - assert exprs[b+2].write is u - assert exprs[b+3].write is v + assert exprs[b].write is u + assert exprs[b+1].write is v + calls = FindNodes(Call).visit(op._func_table['copy_to_host0'].root) + stores = [str(i) for i in calls if i.name == '__atomic_store_n'] + assert stores[:2] == ['__atomic_store_n(&lock0[0],1,__ATOMIC_RELEASE);', + '__atomic_store_n(&lock1[0],1,__ATOMIC_RELEASE);'] op.apply(time_M=nt-2) @@ -603,19 +613,20 @@ def test_tasking_multi_output(self): sections = FindNodes(Section).visit(op1) assert len(sections) == 2 assert str(sections[0].body[0].body[0].body[0].body[0]) ==\ - 'while(lock0[t2] == 0);' + 'while(__atomic_load_n(&lock0[t2],__ATOMIC_ACQUIRE) == 0);' body = op1._func_table['release_lock0'].root.body - assert 'atomic_thread_fence' in str(body.body[1]) for i in range(3): - assert 'lock0[t' in str(body.body[2 + i]) # Set-lock + assert 'lock0[t' in str(body.body[1 + i]) # Set-lock body = op1._func_table['activate0'].root.body - assert str(body.body[-1]) == 'sdata0[wi0].flag = 2;' + assert str(body.body[-1]) == \ + '__atomic_store_n(&sdata0[wi0].flag,2,__ATOMIC_RELEASE);' assert len(op1._func_table) == 5 exprs = FindNodes(Expression).visit(op1._func_table['copy_to_host0'].root) b = 21 if configuration['language'] == 'openacc' else 20 # No `qid` w/ OMP - for i in range(3): - assert 'lock0[t' in str(exprs[b + i]) - assert exprs[b+3].write is usave + assert exprs[b].write is usave + calls = FindNodes(Call).visit(op1._func_table['copy_to_host0'].root) + stores = [str(i) for i in calls if i.name == '__atomic_store_n'] + assert all('lock0[t' in i for i in stores[:3]) op0.apply(time_M=nt-2) op1.apply(time_M=nt-2, u=u1, usave=usave1) @@ -643,7 +654,7 @@ def test_tasking_lock_placement(self): assert len(sections) == 3 assert sections[0].body[0].body[0].body[0].is_Iteration assert str(sections[1].body[0].body[0].body[0].body[0]) ==\ - 'while(lock0[t1] == 0);' + 'while(__atomic_load_n(&lock0[t1],__ATOMIC_ACQUIRE) == 0);' @pytest.mark.parametrize('opt,ntmps', [ (('buffering', 'streaming', 'orchestrate'), 3), @@ -1243,7 +1254,8 @@ def test_tasking_over_compiler_generated(self): assert len([i for i in FindSymbols().visit(op) if isinstance(i, Lock)]) == 1 sections = FindNodes(Section).visit(op) assert len(sections) == 4 - assert 'while(lock0[t1] == 0)' in str(sections[2].body[0].body[0].body[0]) + assert 'while(__atomic_load_n(&lock0[t1],__ATOMIC_ACQUIRE) == 0)' in \ + str(sections[2].body[0].body[0].body[0]) op0.apply(time_M=nt-1) op1.apply(time_M=nt-1, u=u1, usave=usave1) diff --git a/tests/test_iet.py b/tests/test_iet.py index c2d096918f..60f571705f 100644 --- a/tests/test_iet.py +++ b/tests/test_iet.py @@ -6,13 +6,13 @@ import sympy from devito import ( # noqa - Dimension, Eq, Function, Grid, Operator, TimeFunction, switchconfig + Dimension, Eq, Function, Grid, Operator, SubDomain, TimeFunction, switchconfig ) from devito.ir import SymbolRegistry from devito.ir.iet import ( Call, Callable, CGen, Conditional, Definition, Dereference, DeviceCall, DummyExpr, ElementalFunction, FindNodes, FindSymbols, Iteration, KernelLaunch, Lambda, List, - Switch, ThreadFence, Transformer, filter_iterations, make_callable, make_efunc, + Switch, Transformer, filter_iterations, make_callable, make_efunc, retrieve_iteration_tree ) from devito.ir.iet.visitors import sorted_efuncs @@ -120,23 +120,6 @@ def test_nested_calls_cgen(): assert str(code) == 'foo(bar());' -def test_thread_fence_cgen(): - """ - A fence renders as the builtin, which is valid in C and C++ alike. - - Threads that hand work to each other through a shared flag need one either - side of the handshake; without them the stores can be observed out of order - and a hand-off is lost. - """ - assert str(CGen().visit(ThreadFence('acquire'))) == \ - '__atomic_thread_fence(__ATOMIC_ACQUIRE);' - assert str(CGen().visit(ThreadFence('release'))) == \ - '__atomic_thread_fence(__ATOMIC_RELEASE);' - - with pytest.raises(AssertionError): - ThreadFence('seq_cst') - - @pytest.mark.parametrize('mode,expected', [ ('basics', '["x"]'), ('symbolics', '["f"]') @@ -706,3 +689,47 @@ def test_switch_case(): break; } }""" + + +@pytest.mark.parametrize('mode', ['save', 'prefetch']) +def test_async_handshake_atomics(mode): + """ + The locks and flags of an asynchronous task are accessed with acquire loads + and release stores, or a weakly ordered CPU may e.g. see the lock released + before the data it guards. + """ + grid = Grid(shape=(4, 4)) + nt = 10 + + if mode == 'save': + class Interior(SubDomain): + name = 'interior' + + def define(self, dimensions): + x, y = dimensions + return {x: ('middle', 1, 1), y: y} + + interior = Interior(grid=grid) + tmp = Function(name='tmp', grid=grid) + u = TimeFunction(name='u', grid=grid, save=nt) + v = TimeFunction(name='v', grid=grid) + eqns = [Eq(tmp, v), Eq(v.forward, v + 1), + Eq(u.forward, tmp, subdomain=interior)] + opt = ('tasking', 'orchestrate') + else: + w = TimeFunction(name='w', grid=grid) + wsave = TimeFunction(name='wsave', grid=grid, save=nt) + eqns = [Eq(w.forward, w + wsave)] + opt = ('buffering', 'streaming', 'orchestrate') + + op = Operator(eqns, opt=opt, platform='nvidiaX', language='openmp', + name='Handshake') + + code = str(op) + assert 'while(__atomic_load_n(&lock0[0],__ATOMIC_ACQUIRE) == 0);' in code + assert '__atomic_store_n(&lock0[0],2,__ATOMIC_RELEASE);' in code + assert '__atomic_store_n(&sdata0->flag,1,__ATOMIC_RELEASE);' in code + assert 'while(__atomic_load_n(&lock0[0],__ATOMIC_ACQUIRE) != 2);' in code + assert '__atomic_store_n(&lock0[0],0,__ATOMIC_RELEASE);' in code + assert '__atomic_store_n(&sdata0[0].flag,2,__ATOMIC_RELEASE);' in code + assert '__atomic_thread_fence' not in code