Skip to content

Commit

Permalink
Introduce the Program class for lambda support
Browse files Browse the repository at this point in the history
A lambda function can be defined using the Symbol class and the Program convert it to a series of instruction that can be executed by any backend
  • Loading branch information
mickeyasa authored Nov 26, 2024
1 parent 0ca8db0 commit 7a36c46
Show file tree
Hide file tree
Showing 4 changed files with 432 additions and 1 deletion.
102 changes: 102 additions & 0 deletions icicle/backend/cpu/include/cpu_program_executor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#pragma once

#include <vector>
#include <functional>
#include "icicle/program/symbol.h"
#include "icicle/program/program.h"

namespace icicle {

template <typename S>
class CpuProgramExecutor
{
public:
CpuProgramExecutor(const Program<S>& program)
: m_program(program), m_variable_ptrs(program.get_nof_vars()), m_intermediates(program.m_nof_intermidiates)
{
// initialize m_variable_ptrs vector
int variable_ptrs_idx = program.m_nof_inputs + program.m_nof_outputs;
for (int idx = 0; idx < program.m_nof_constants; ++idx) {
m_variable_ptrs[variable_ptrs_idx++] = (S*)(&(program.m_constants[idx]));
}
for (int idx = 0; idx < program.m_nof_intermidiates; ++idx) {
m_variable_ptrs[variable_ptrs_idx++] = &(m_intermediates[idx]);
}
}

// execute the program an return a program to the result
void execute()
{
const std::byte* instruction;
for (InstructionType instruction : m_program.m_instructions) {
const int func_select = Program<S>::get_opcode(instruction);
(this->*m_function_arr[func_select])(instruction);
}
}

std::vector<S*> m_variable_ptrs;

private:
const Program<S>& m_program;
std::vector<S> m_intermediates;

// exe functions
void exe_add(const InstructionType instruction)
{
const std::byte* inst_arr = reinterpret_cast<const std::byte*>(&instruction);
*m_variable_ptrs[(int)inst_arr[Program<S>::INST_RESULT]] =
*m_variable_ptrs[(int)inst_arr[Program<S>::INST_OPERAND1]] +
*m_variable_ptrs[(int)inst_arr[Program<S>::INST_OPERAND2]];
}

void exe_mult(const InstructionType instruction)
{
const std::byte* inst_arr = reinterpret_cast<const std::byte*>(&instruction);
*m_variable_ptrs[(int)inst_arr[Program<S>::INST_RESULT]] =
*m_variable_ptrs[(int)inst_arr[Program<S>::INST_OPERAND1]] *
*m_variable_ptrs[(int)inst_arr[Program<S>::INST_OPERAND2]];
}

void exe_sub(const InstructionType instruction)
{
const std::byte* inst_arr = reinterpret_cast<const std::byte*>(&instruction);
*m_variable_ptrs[(int)inst_arr[Program<S>::INST_RESULT]] =
*m_variable_ptrs[(int)inst_arr[Program<S>::INST_OPERAND1]] -
*m_variable_ptrs[(int)inst_arr[Program<S>::INST_OPERAND2]];
}

void exe_inverse(const InstructionType instruction)
{
const std::byte* inst_arr = reinterpret_cast<const std::byte*>(&instruction);
*m_variable_ptrs[(int)inst_arr[Program<S>::INST_RESULT]] =
S::inverse(*m_variable_ptrs[(int)inst_arr[Program<S>::INST_OPERAND1]]);
}

void exe_predef_ab_minus_c(const InstructionType instruction)
{
const S& a = *m_variable_ptrs[0];
const S& b = *m_variable_ptrs[1];
const S& c = *m_variable_ptrs[2];
*m_variable_ptrs[3] = a * b - c;
}
void exe_predef_eq_x_ab_minus_c(const InstructionType instruction)
{
const S& a = *m_variable_ptrs[0];
const S& b = *m_variable_ptrs[1];
const S& c = *m_variable_ptrs[2];
const S& eq = *m_variable_ptrs[3];
*m_variable_ptrs[4] = eq * (a * b - c);
}

using FunctionPtr = void (CpuProgramExecutor::*)(const InstructionType);
inline static const FunctionPtr m_function_arr[] = {
&CpuProgramExecutor::exe_add, // OP_ADD
&CpuProgramExecutor::exe_mult, // OP_MULT
&CpuProgramExecutor::exe_sub, // OP_SUB
&CpuProgramExecutor::exe_inverse, // OP_INV
// pre defined functions
&CpuProgramExecutor::exe_predef_ab_minus_c, // predef A*B-C
&CpuProgramExecutor::exe_predef_eq_x_ab_minus_c}; // predef EQ*(A*B-C)
};

} // namespace icicle
2 changes: 1 addition & 1 deletion icicle/backend/cpu/src/field/cpu_vec_ops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ class VectorOpTask : public TaskBase
public:
T m_intermidiate_res; // pointer to the output. Can be a vector or scalar pointer
uint64_t m_idx_in_batch; // index in the batch. Used in intermediate res tasks
};
}; // class VectorOpTask

#define NOF_OPERATIONS_PER_TASK 512
#define CONFIG_NOF_THREADS_KEY "n_threads"
Expand Down
161 changes: 161 additions & 0 deletions icicle/include/icicle/program/program.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#pragma once

#include <vector>
#include <functional>
#include "icicle/program/symbol.h"

namespace icicle {

using InstructionType = uint32_t;

enum PreDefinedPrograms {
AB_MINUS_C = 0, // (A*B)-C
EQ_X_AB_MINUS_C // E*(A*B-C)
};

/**
* @brief A class that convert the function described by user into a program that can be executed
*
* This class receives a Symbol instance that contains a DFG representing the required calculation.
* It generates a vector of instructions that represent the calculation.
* Each instruction has the following format.
* bits 7:0 - opcode according to enum ProgramOpcode
* bits 15:8 - operand 1 selector from the input vector
* bits 23:16 - operand 2 selector from the input vector
* bits 31:24 - result selector
*/
template <typename S>
class Program
{
public:
// Generate a program based on a lambda function
Program(std::function<Symbol<S>(std::vector<Symbol<S>>&)> program_func, const int nof_inputs)
{
std::vector<Symbol<S>> program_inputs(nof_inputs);
set_as_inputs(program_inputs);
Symbol<S> result = program_func(program_inputs);
generate_program(result);
}

// Generate a program based on a PreDefinedPrograms
Program(PreDefinedPrograms pre_def)
{
switch (pre_def) {
case AB_MINUS_C:
m_nof_inputs = 3;
break;
case EQ_X_AB_MINUS_C:
m_nof_inputs = 4;
break;
default:
ICICLE_LOG_ERROR << "Illegal opcode: " << int(pre_def);
}
m_nof_outputs = 1;
int instruction = int(ProgramOpcode::NOF_OPERATIONS) + int(pre_def);
m_instructions.push_back(instruction);
}

// run over all inputs at the vector and set their operands to OP_INPUT
void set_as_inputs(std::vector<Symbol<S>>& combine_inputs)
{
m_nof_inputs = combine_inputs.size();
for (int input_idx = 0; input_idx < m_nof_inputs; input_idx++) {
combine_inputs[input_idx].set_as_input(input_idx);
}
}

// run over the DFG held by result and gemerate the program
void generate_program(Symbol<S>& result)
{
m_nof_outputs = 1;
result.m_operation->m_variable_idx = m_nof_inputs;
Operation<S>::reset_visit();
allocate_constants(result.m_operation);
Operation<S>::reset_visit();
generate_program(result.m_operation);
}

// Program
std::vector<InstructionType> m_instructions;
std::vector<S> m_constants;
int m_nof_inputs = 0;
int m_nof_outputs = 0;
int m_nof_constants = 0;
int m_nof_intermidiates = 0;

const int get_nof_vars() const { return m_nof_inputs + m_nof_outputs + m_nof_constants + m_nof_intermidiates; }

static inline const int INST_OPCODE = 0;
static inline const int INST_OPERAND1 = 1;
static inline const int INST_OPERAND2 = 2;
static inline const int INST_RESULT = 3;
inline static int get_opcode(const InstructionType instruction) { return (instruction & 0xFF); }

private:
void generate_program(std::shared_ptr<Operation<S>> operation)
{
if (
operation == nullptr || operation->is_visited(true) || operation->m_opcode == OP_INPUT ||
operation->m_opcode == OP_CONST)
return;
generate_program(operation->m_operand1);
generate_program(operation->m_operand2);

// Build an instruction
std::byte int_arr[sizeof(InstructionType)] = {};
// Set instruction::opcode
int_arr[INST_OPCODE] = std::byte(operation->m_opcode);
// Set instruction::operand1
int_arr[INST_OPERAND1] = std::byte(operation->m_operand1->m_variable_idx);

if (operation->m_operand2) {
// Set instruction::operand2
int_arr[INST_OPERAND2] = std::byte(operation->m_operand2->m_variable_idx);
}

if (operation->m_variable_idx < 0) {
// allocate a register for the result
operation->m_variable_idx = allocate_intermidiate();
}

// Set instruction::operand2
int_arr[INST_RESULT] = std::byte(operation->m_variable_idx);
InstructionType instruction;
std::memcpy(&instruction, int_arr, sizeof(InstructionType));
m_instructions.push_back(instruction);
}

void allocate_constants(std::shared_ptr<Operation<S>> operation)
{
if (operation == nullptr || operation->is_visited(true)) return;
allocate_constants(operation->m_operand1);
allocate_constants(operation->m_operand2);
if (operation->m_opcode == OP_CONST) {
m_constants.push_back(*(operation->m_constant));
operation->m_variable_idx = allocate_constant();
}
}

int allocate_constant() { return (m_nof_inputs + m_nof_outputs + m_nof_constants++); }
int allocate_intermidiate() { return (m_nof_inputs + m_nof_outputs + m_nof_constants + m_nof_intermidiates++); }

public:
void print_program()
{
std::cout << "nof_inputs: " << m_nof_inputs << std::endl;
std::cout << "nof_outputs: " << m_nof_outputs << std::endl;
std::cout << "nof_constants: " << m_nof_constants << std::endl;
std::cout << "nof_intermidiates: " << m_nof_intermidiates << std::endl;
std::cout << "Constants:: " << std::endl;
for (auto constant : m_constants) {
std::cout << " " << constant << std::endl;
}
std::cout << "Instructions:: " << std::endl;
for (auto inst : m_instructions) {
std::cout << " Opcode: " << (inst & 0xFF) << ", op1: " << ((inst >> 8) & 0xFF)
<< ", op2: " << ((inst >> 16) & 0xFF) << ", Res: " << ((inst >> 24) & 0xFF) << std::endl;
}
}
};

} // namespace icicle
Loading

0 comments on commit 7a36c46

Please sign in to comment.