#include <atomic>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#ifdef FXT_GEMM_THREADS
#include <condition_variable>
#include <functional>
#include <mutex>
#include <thread>
#include <vector>
#endif
#include "gemmology.h"
namespace {
#if defined(FXT_GEMM_I8MM)
using Arch = xsimd::i8mm<xsimd::neon64>;
#elif defined(FXT_GEMM_AVX2)
using Arch = xsimd::avx2;
#else
#error "fxtranslate: build.rs must define the gemmology arch (FXT_GEMM_I8MM or FXT_GEMM_AVX2)"
#endif
std::atomic<size_t> g_prepared_bytes{0};
constexpr size_t kColStride = 8;
constexpr size_t kRegElems = xsimd::batch<int8_t, Arch>::size;
inline size_t round_up(size_t x, size_t m) { return (x + m - 1) / m * m; }
inline void *aligned(size_t bytes) {
size_t n = round_up(bytes ? bytes : 1, 64);
return std::aligned_alloc(64, n);
}
struct PreparedB {
int8_t *data; size_t n; size_t n_pad; size_t k; };
#ifdef FXT_GEMM_THREADS
class GemmPool {
public:
static GemmPool &instance() {
static GemmPool pool;
return pool;
}
size_t size() const { return nthreads_; }
template <class F>
void operator()(size_t start, size_t end, size_t stride, F &&f) {
if (end <= start) return;
const size_t iters = (end - start + stride - 1) / stride;
const size_t nt = iters < nthreads_ ? iters : nthreads_;
if (nt <= 1) { for (size_t i = start; i < end; i += stride) f(i);
return;
}
std::function<void(size_t)> job = [&](size_t i) { f(i); };
const size_t chunk = (iters + nt - 1) / nt; {
std::unique_lock<std::mutex> lk(mtx_);
job_ = &job;
start_ = start;
stride_ = stride;
iters_ = iters;
chunk_ = chunk;
ntasks_ = nt;
remaining_ = nt - 1; ++generation_;
cv_work_.notify_all();
}
run_stripe(0); std::unique_lock<std::mutex> lk(mtx_);
cv_done_.wait(lk, [&] { return remaining_ == 0; });
job_ = nullptr;
}
private:
GemmPool() {
const char *env = std::getenv("FXT_GEMM_THREADS");
long n = env ? std::strtol(env, nullptr, 10) : 1;
if (n < 1) n = 1;
nthreads_ = static_cast<size_t>(n);
for (size_t w = 1; w < nthreads_; ++w)
workers_.emplace_back([this, w] { worker_loop(w); });
}
~GemmPool() {
{
std::unique_lock<std::mutex> lk(mtx_);
stop_ = true;
cv_work_.notify_all();
}
for (auto &t : workers_) t.join();
}
void run_stripe(size_t w) {
const size_t lo = w * chunk_;
size_t hi = lo + chunk_;
if (hi > iters_) hi = iters_;
for (size_t it = lo; it < hi; ++it) (*job_)(start_ + it * stride_);
}
void worker_loop(size_t w) {
size_t seen = 0;
for (;;) {
std::unique_lock<std::mutex> lk(mtx_);
cv_work_.wait(lk, [&] { return stop_ || generation_ != seen; });
if (stop_) return;
seen = generation_;
const bool has_work = w < ntasks_;
lk.unlock();
if (has_work) run_stripe(w);
if (has_work) {
lk.lock();
if (--remaining_ == 0) cv_done_.notify_one();
}
}
}
size_t nthreads_ = 1;
std::vector<std::thread> workers_;
std::mutex mtx_;
std::condition_variable cv_work_, cv_done_;
bool stop_ = false;
size_t generation_ = 0;
std::function<void(size_t)> *job_ = nullptr;
size_t start_ = 0, stride_ = 0, iters_ = 0, chunk_ = 0, ntasks_ = 0, remaining_ = 0;
};
constexpr size_t kParallelMinWork = size_t{1} << 22;
#endif
}
extern "C" {
void *gemmology_prepare_b(const int8_t *b_transposed, size_t n, size_t k) {
if (k % kRegElems != 0) return nullptr;
const size_t n_pad = round_up(n, kColStride);
int8_t *src = static_cast<int8_t *>(aligned(n_pad * k));
std::memset(src, 0, round_up(n_pad * k, 64));
std::memcpy(src, b_transposed, n * k);
int8_t *packed = static_cast<int8_t *>(aligned(n_pad * k));
gemmology::PrepareBQuantizedTransposed<Arch>(src, packed, k,
n_pad);
std::free(src);
g_prepared_bytes.fetch_add(n_pad * k, std::memory_order_relaxed);
return new PreparedB{packed, n, n_pad, k};
}
void gemmology_free_b(void *handle) {
if (!handle) return;
PreparedB *h = static_cast<PreparedB *>(handle);
g_prepared_bytes.fetch_sub(h->n_pad * h->k, std::memory_order_relaxed);
std::free(h->data);
delete h;
}
size_t gemmology_prepared_bytes() {
return g_prepared_bytes.load(std::memory_order_relaxed);
}
size_t gemmology_gemm_threads() {
#ifdef FXT_GEMM_THREADS
return GemmPool::instance().size();
#else
return 0;
#endif
}
const char *gemmology_backend_name() { return Arch::name(); }
void gemmology_read_row(const void *handle, size_t id, int8_t *out) {
const PreparedB *h = static_cast<const PreparedB *>(handle);
const size_t kblocks = h->k / kRegElems;
const size_t row_block = id / kColStride;
const size_t row_in = id % kColStride;
for (size_t cb = 0; cb < kblocks; ++cb) {
const size_t reg = (row_block * kblocks + cb) * kColStride + row_in;
std::memcpy(out + cb * kRegElems, h->data + reg * kRegElems, kRegElems);
}
}
void gemmology_multiply(void *handle, const uint8_t *a, size_t m, float unquant,
const float *bias, float *out) {
const PreparedB *h = static_cast<const PreparedB *>(handle);
const size_t k = h->k, n = h->n, n_pad = h->n_pad;
uint8_t *a_al = static_cast<uint8_t *>(aligned(m * k));
std::memcpy(a_al, a, m * k);
float *bias_al = static_cast<float *>(aligned(n_pad * sizeof(float)));
std::memcpy(bias_al, bias, n * sizeof(float));
for (size_t j = n; j < n_pad; ++j) bias_al[j] = 0.0f;
float *out_al = static_cast<float *>(aligned(m * n_pad * sizeof(float)));
auto callback =
gemmology::callbacks::UnquantizeAndAddBiasAndWrite(unquant, bias_al, out_al);
#ifdef FXT_GEMM_THREADS
GemmPool &pool = GemmPool::instance();
if (pool.size() > 1 && m * k * n_pad >= kParallelMinWork) {
gemmology::Shift::Multiply<Arch>(a_al, h->data, m, k, n_pad, callback, pool);
} else {
gemmology::Shift::Multiply<Arch>(a_al, h->data, m, k, n_pad, callback);
}
#else
gemmology::Shift::Multiply<Arch>(a_al, h->data, m, k, n_pad, callback);
#endif
for (size_t r = 0; r < m; ++r)
std::memcpy(out + r * n, out_al + r * n_pad, n * sizeof(float));
std::free(a_al);
std::free(bias_al);
std::free(out_al);
}
}