Skip to content
This repository was archived by the owner on Mar 25, 2025. It is now read-only.

Commit 265a02b

Browse files
[LLVM][GPU] Atomic updates support (#853)
1. Helper visitor generates atomic statements with += and -= once again. 2. LLVM visitor now knows how to lower atomic statements for CPUs (trivially) and GPUs. 3. A corresponding IR test was added. `expsyn` test on GPU enabled Co-authored-by: Ioannis Magkanaris <iomagkanaris@gmail.com>
1 parent 95782bc commit 265a02b

File tree

7 files changed

+139
-65
lines changed

7 files changed

+139
-65
lines changed

src/codegen/llvm/codegen_llvm_helper_visitor.cpp

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ static void append_statements_from_block(ast::StatementVector& statements,
283283
* Create atomic statement for given expression of the form a[i] += expression
284284
* @param var Name of the variable on the LHS (it's an array), e.g. `a`
285285
* @param var_index Name of the index variable to access variable `var` e.g. `i`
286-
* @param op_str Operators like += or -=
286+
* @param op_str Operators like =, += or -=
287287
* @param rhs_str expression that will be added or subtracted from `var[var_index]`
288288
* @return A statement representing atomic operation using `ast::CodegenAtomicStatement`
289289
*/
@@ -299,23 +299,9 @@ static std::shared_ptr<ast::CodegenAtomicStatement> create_atomic_statement(
299299
/*at=*/nullptr,
300300
/*index=*/nullptr);
301301

302-
// LLVM IR generation is now only supporting assignment (=) and not += or -=
303-
// So we need to write increment operation a += b as an assignment operation
304-
// a = a + b.
305-
// See https://github.com/BlueBrain/nmodl/issues/851
306-
307-
std::string op(op_str);
308-
stringutils::remove_character(op, '=');
309-
310-
// make sure only + or - operator is used
311-
if (op_str != "-" && op_str != "+") {
312-
throw std::runtime_error("Unsupported binary operator for atomic statement");
313-
}
314-
315-
auto* rhs = create_expression("{}[{}] {} {} "_format(var, var_index, op, rhs_str));
316-
return std::make_shared<ast::CodegenAtomicStatement>(lhs,
317-
ast::BinaryOperator{ast::BOP_ASSIGN},
318-
rhs);
302+
auto op = ast::BinaryOperator(ast::string_to_binaryop(op_str));
303+
auto rhs = create_expression(rhs_str);
304+
return std::make_shared<ast::CodegenAtomicStatement>(lhs, op, rhs);
319305
}
320306

321307
/**
@@ -420,22 +406,7 @@ void CodegenLLVMHelperVisitor::ion_write_statements(BlockType type,
420406
index_statements.push_back(visitor::create_statement(index_statement));
421407

422408
// pass ion variable to write and its index
423-
424-
// lhs variable
425-
std::string lhs = "{}[{}] "_format(ion_varname, index_varname);
426-
427-
// lets turn a += b into a = a + b if applicable
428-
// note that this is done in order to facilitate existing implementation in the llvm
429-
// backend which doesn't support += or -= operators.
430-
std::string statement;
431-
if (!op.compare("+=")) {
432-
statement = "{} = {} + {}"_format(lhs, lhs, rhs);
433-
} else if (!op.compare("-=")) {
434-
statement = "{} = {} - {}"_format(lhs, lhs, rhs);
435-
} else {
436-
statement = "{} {} {}"_format(lhs, op, rhs);
437-
}
438-
body_statements.push_back(visitor::create_statement(statement));
409+
body_statements.push_back(create_atomic_statement(ion_varname, index_varname, op, rhs));
439410
};
440411

441412
/// iterate over all ions and create write ion statements for given block type

src/codegen/llvm/codegen_llvm_visitor.cpp

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -512,31 +512,73 @@ void CodegenLLVMVisitor::visit_boolean(const ast::Boolean& node) {
512512
ir_builder.create_boolean_constant(node.get_value());
513513
}
514514

515-
/**
516-
* Currently, this functions is very similar to visiting the binary operator. However, the
517-
* difference here is that the writes to the LHS variable must be atomic. These has a particular
518-
* use case in synapse kernels. For simplicity, we choose not to support atomic writes at this
519-
* stage and emit a warning.
520-
*
521-
* \todo support this properly.
522-
*/
523515
void CodegenLLVMVisitor::visit_codegen_atomic_statement(const ast::CodegenAtomicStatement& node) {
524-
if (platform.is_cpu_with_simd())
525-
logger->warn("Atomic operations are not supported");
516+
// Get the variable node that need an atomic update.
517+
const auto& var = std::dynamic_pointer_cast<ast::VarName>(node.get_lhs());
518+
if (!var)
519+
throw std::runtime_error("Error: only 'VarName' update is supported\n");
526520

527-
// Support only assignment for now.
521+
// Evaluate RHS of the update.
528522
llvm::Value* rhs = accept_and_get(node.get_rhs());
529-
if (node.get_atomic_op().get_value() != ast::BinaryOp::BOP_ASSIGN)
530-
throw std::runtime_error(
531-
"Error: only assignment is supported for CodegenAtomicStatement\n");
532-
const auto& var = dynamic_cast<ast::VarName*>(node.get_lhs().get());
533-
if (!var)
534-
throw std::runtime_error("Error: only 'VarName' assignment is supported\n");
535523

536-
// Process the assignment as if it was non-atomic.
537-
if (platform.is_cpu_with_simd())
538-
logger->warn("Treating write as non-atomic");
539-
write_to_variable(*var, rhs);
524+
// First, check if it is an atomic write only and we can return early.
525+
// Otherwise, extract what kind of atomic update we want to make.
526+
ast::BinaryOp atomic_op = node.get_atomic_op().get_value();
527+
if (atomic_op == ast::BinaryOp::BOP_ASSIGN) {
528+
write_to_variable(*var, rhs);
529+
return;
530+
}
531+
ast::BinaryOp op = ir_builder.extract_atomic_op(atomic_op);
532+
533+
// For different platforms, we handle atomic updates differently!
534+
if (platform.is_cpu_with_simd()) {
535+
throw std::runtime_error("Error: no atomic update support for SIMD CPUs\n");
536+
} else if (platform.is_gpu()) {
537+
const auto& identifier = var->get_name();
538+
539+
// We only need to support atomic updates to instance struct members.
540+
if (!identifier->is_codegen_instance_var())
541+
throw std::runtime_error("Error: atomic updates for non-instance variable\n");
542+
543+
const auto& node = std::dynamic_pointer_cast<ast::CodegenInstanceVar>(identifier);
544+
const auto& instance_name = node->get_instance_var()->get_node_name();
545+
const auto& member_node = node->get_member_var();
546+
const auto& member_name = member_node->get_node_name();
547+
548+
if (!instance_var_helper.is_an_instance_variable(member_name))
549+
throw std::runtime_error("Error: " + member_name +
550+
" is not a member of the instance variable\n");
551+
552+
llvm::Value* instance_ptr = ir_builder.create_load(instance_name);
553+
int member_index = instance_var_helper.get_variable_index(member_name);
554+
llvm::Value* member_ptr = ir_builder.get_struct_member_ptr(instance_ptr, member_index);
555+
556+
// Some sanity checks.
557+
auto codegen_var_with_type = instance_var_helper.get_variable(member_name);
558+
if (!codegen_var_with_type->get_is_pointer())
559+
throw std::runtime_error(
560+
"Error: atomic updates are allowed on pointer variables only\n");
561+
const auto& member_var_name = std::dynamic_pointer_cast<ast::VarName>(member_node);
562+
if (!member_var_name->get_name()->is_indexed_name())
563+
throw std::runtime_error("Error: " + member_name + " is not an IndexedName\n");
564+
const auto& member_indexed_name = std::dynamic_pointer_cast<ast::IndexedName>(
565+
member_var_name->get_name());
566+
if (!member_indexed_name->get_length()->is_name())
567+
throw std::runtime_error("Error: " + member_name + " must be indexed with a variable!");
568+
569+
llvm::Value* i64_index = get_index(*member_indexed_name);
570+
llvm::Value* instance_member = ir_builder.create_load(member_ptr);
571+
llvm::Value* ptr = ir_builder.create_inbounds_gep(instance_member, i64_index);
572+
573+
ir_builder.create_atomic_op(ptr, rhs, op);
574+
} else {
575+
// For non-SIMD CPUs, updates don't have to be atomic at all!
576+
llvm::Value* lhs = accept_and_get(node.get_lhs());
577+
ir_builder.create_binary_op(lhs, rhs, op);
578+
llvm::Value* result = ir_builder.pop_last_value();
579+
580+
write_to_variable(*var, result);
581+
}
540582
}
541583

542584
// Generating FOR loop in LLVM IR creates the following structure:

src/codegen/llvm/llvm_ir_builder.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,28 @@ void IRBuilder::create_array_alloca(const std::string& name,
293293
create_alloca(name, array_type);
294294
}
295295

296+
ast::BinaryOp IRBuilder::extract_atomic_op(ast::BinaryOp op) {
297+
switch (op) {
298+
case ast::BinaryOp::BOP_SUB_ASSIGN:
299+
return ast::BinaryOp::BOP_SUBTRACTION;
300+
case ast::BinaryOp::BOP_ADD_ASSIGN:
301+
return ast::BinaryOp::BOP_ADDITION;
302+
default:
303+
throw std::runtime_error("Error: only atomic addition and subtraction is supported\n");
304+
}
305+
}
306+
307+
void IRBuilder::create_atomic_op(llvm::Value* ptr, llvm::Value* update, ast::BinaryOp op) {
308+
if (op == ast::BinaryOp::BOP_SUBTRACTION) {
309+
update = builder.CreateFNeg(update);
310+
}
311+
builder.CreateAtomicRMW(llvm::AtomicRMWInst::FAdd,
312+
ptr,
313+
update,
314+
llvm::MaybeAlign(),
315+
llvm::AtomicOrdering::SequentiallyConsistent);
316+
}
317+
296318
void IRBuilder::create_binary_op(llvm::Value* lhs, llvm::Value* rhs, ast::BinaryOp op) {
297319
// Check that both lhs and rhs have the same types.
298320
if (lhs->getType() != rhs->getType())

src/codegen/llvm/llvm_ir_builder.hpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ class IRBuilder {
146146
return vectorize && mask;
147147
}
148148

149+
/// Extracts binary operator (+ or -) from atomic update (+= or =-).
150+
ast::BinaryOp extract_atomic_op(ast::BinaryOp op);
151+
149152
/// Generates LLVM IR to allocate the arguments of the function on the stack.
150153
void allocate_function_arguments(llvm::Function* function,
151154
const ast::CodegenVarWithTypeVector& nmodl_arguments);
@@ -158,6 +161,9 @@ class IRBuilder {
158161
/// Generates LLVM IR for the given binary operator.
159162
void create_binary_op(llvm::Value* lhs, llvm::Value* rhs, ast::BinaryOp op);
160163

164+
/// Generates LLVM IR for the given atomic operator.
165+
void create_atomic_op(llvm::Value* ptr, llvm::Value* update, ast::BinaryOp op);
166+
161167
/// Generates LLVM IR for the bitcast instruction.
162168
llvm::Value* create_bitcast(llvm::Value* value, llvm::Type* dst_type);
163169

@@ -304,13 +310,13 @@ class IRBuilder {
304310
/// Pops the last visited value from the value stack.
305311
llvm::Value* pop_last_value();
306312

313+
/// Generates an inbounds GEP instruction for the given value and returns calculated address.
314+
llvm::Value* create_inbounds_gep(llvm::Value* variable, llvm::Value* index);
315+
307316
private:
308317
/// Generates an inbounds GEP instruction for the given name and returns calculated address.
309318
llvm::Value* create_inbounds_gep(const std::string& variable_name, llvm::Value* index);
310319

311-
/// Generates an inbounds GEP instruction for the given value and returns calculated address.
312-
llvm::Value* create_inbounds_gep(llvm::Value* variable, llvm::Value* index);
313-
314320
/// Returns a scalar constant of the provided type.
315321
template <typename C, typename V>
316322
llvm::Value* get_scalar_constant(llvm::Type* type, V value);

test/benchmark/CMakeLists.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@ if(NMODL_ENABLE_PYTHON_BINDINGS)
4444
set_tests_properties(
4545
"PyJIT/${modfile_name}" PROPERTIES ENVIRONMENT
4646
PYTHONPATH=${PROJECT_BINARY_DIR}/lib:$ENV{PYTHONPATH})
47-
# Disable running the expsyn.mod on GPU because atomic instructions are not supported yet on GPU
48-
# See https://github.com/BlueBrain/nmodl/issues/834
49-
if(NMODL_ENABLE_LLVM_CUDA AND NOT ${modfile} STREQUAL "${NMODL_PROJECT_SOURCE_DIR}/test/benchmark/kernels/expsyn.mod")
47+
48+
if(NMODL_ENABLE_LLVM_CUDA)
5049
add_test(NAME "PyJIT/${modfile_name}_gpu"
5150
COMMAND ${PYTHON_EXECUTABLE} ${NMODL_PROJECT_SOURCE_DIR}/test/benchmark/benchmark.py
5251
--file ${modfile} --gpu ${extra_args})

test/unit/codegen/codegen_llvm_ir.cpp

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1360,7 +1360,7 @@ SCENARIO("Vectorised derivative block", "[visitor][llvm][derivative]") {
13601360
}
13611361
g = (g-rhs)/0.001
13621362
mech->ion_dinadv[ion_dinadv_id] = mech->ion_dinadv[ion_dinadv_id]+(dina-mech->ina[id])/0.001
1363-
mech->ion_ina[ion_ina_id] = mech->ion_ina[ion_ina_id]+mech->ina[id]
1363+
mech->ion_ina[ion_ina_id] += mech->ina[id]
13641364
mech->vec_rhs[node_id] = mech->vec_rhs[node_id]-rhs
13651365
mech->vec_d[node_id] = mech->vec_d[node_id]+g
13661366
})";
@@ -1807,4 +1807,38 @@ SCENARIO("GPU kernel body IR generation", "[visitor][llvm][gpu]") {
18071807
REQUIRE(!std::regex_search(module_string, m, log_old_call));
18081808
}
18091809
}
1810+
1811+
GIVEN("For current update with atomic addition ") {
1812+
std::string nmodl_text = R"(
1813+
NEURON {
1814+
SUFFIX test
1815+
USEION na READ ena WRITE ina
1816+
}
1817+
1818+
STATE { }
1819+
1820+
ASSIGNED {
1821+
v (mV)
1822+
ena (mV)
1823+
ina (mA/cm2)
1824+
}
1825+
1826+
BREAKPOINT {
1827+
SOLVE states METHOD cnexp
1828+
}
1829+
1830+
DERIVATIVE states { }
1831+
)";
1832+
1833+
THEN("corresponding LLVM atomic instruction is generated") {
1834+
std::string module_string = run_gpu_llvm_visitor(nmodl_text,
1835+
/*opt_level=*/0,
1836+
/*use_single_precision=*/false);
1837+
std::smatch m;
1838+
1839+
// Check for atomic addition.
1840+
std::regex add(R"(atomicrmw fadd double\* %.*, double %.* seq_cst)");
1841+
REQUIRE(std::regex_search(module_string, m, add));
1842+
}
1843+
}
18101844
}

test/unit/codegen/codegen_llvm_visitor.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -462,8 +462,8 @@ SCENARIO("Channel: Derivative and breakpoint block llvm transformations",
462462
g = (g-rhs)/0.001
463463
mech->ion_dinadv[ion_dinadv_id] = mech->ion_dinadv[ion_dinadv_id]+(dina-mech->ina[id])/0.001
464464
mech->ion_dikdv[ion_dikdv_id] = mech->ion_dikdv[ion_dikdv_id]+(dik-mech->ik[id])/0.001
465-
mech->ion_ina[ion_ina_id] = mech->ion_ina[ion_ina_id]+mech->ina[id]
466-
mech->ion_ik[ion_ik_id] = mech->ion_ik[ion_ik_id]+mech->ik[id]
465+
mech->ion_ina[ion_ina_id] += mech->ina[id]
466+
mech->ion_ik[ion_ik_id] += mech->ik[id]
467467
mech->vec_rhs[node_id] = mech->vec_rhs[node_id]-rhs
468468
mech->vec_d[node_id] = mech->vec_d[node_id]+g
469469
}
@@ -593,7 +593,7 @@ SCENARIO("Synapse: Derivative and breakpoint block llvm transformations",
593593
}
594594
mech->g[id] = (mech->g[id]-rhs)/0.001
595595
mech->ion_dinadv[ion_dinadv_id] = mech->ion_dinadv[ion_dinadv_id]+(dina-mech->ina[id])/0.001*1.e2/mech->node_area[node_area_id]
596-
mech->ion_ina[ion_ina_id] = mech->ion_ina[ion_ina_id]+mech->ina[id]*(1.e2/mech->node_area[node_area_id])
596+
mech->ion_ina[ion_ina_id] += mech->ina[id]*(1.e2/mech->node_area[node_area_id])
597597
mfactor = 1.e2/mech->node_area[node_area_id]
598598
mech->g[id] = mech->g[id]*mfactor
599599
rhs = rhs*mfactor

0 commit comments

Comments
 (0)