// Oasis engine: loads an .oasis model and describes RGB images as JSON records, in one C++17 translation // unit. `python oasis.py build` compiles it into the library that oasis.py loads, as // c++ -std=c++17 -O3 -ffast-math -fno-finite-math-only -march=native -fopenmp -fPIC -shared -fvisibility=hidden oasis.cpp -o liboasis.so // cl /std:c++17 /O2 /fp:fast /arch:AVX2 /openmp /EHsc /LD oasis.cpp /Fe:oasis.dll // with -mcpu=native in place of -march=native for Clang on 64-bit Arm. The C API below is all it exports. #ifndef OASIS_H #define OASIS_H #ifdef __cplusplus extern "C" { #endif #if defined(_WIN32) #define OASIS_API __declspec(dllexport) #else #define OASIS_API __attribute__((visibility("default"))) #endif typedef struct oasis_model oasis_model; /* Returns NULL on failure; see oasis_last_error(). */ OASIS_API oasis_model* oasis_load(const char* path); OASIS_API void oasis_free(oasis_model* model); /* rgb: packed 8-bit RGB, row-major, width * height * 3 bytes. options_json: NULL or a JSON object with language (en, de, fr, es, it, nl or pl), keywords (a list of words or phrases the description should favour, or one newline-separated string), conf, text_conf, max_objects, max_text_lines, max_tokens, masks, mask_format ("largest", "polygons", "merged", "bitmap", "none"), mask_threshold, nms, reasoning, sections (any of "scene", "objects", "regions", "text", "relations", "events", "reasoning"; text alone skips the decoder), fields (any of "bbox", "score", "mask", "position", "description", "count") and form (an ordered object of fields: {"enum": [...]}, {"type": "integer", "minimum": a, "maximum": b}, {"type": "string", "maxLength": n} or {"x-source": "bbox", "x-of": ""}), which returns {"form": ...} instead of a description. Writes a NUL-terminated JSON record to out; returns its length, or a negative value on error (minus the required size when capacity is too small). */ OASIS_API int oasis_describe(oasis_model* model, const unsigned char* rgb, int width, int height, const char* options_json, char* out, int capacity); OASIS_API const char* oasis_last_error(void); #ifdef __cplusplus } #endif #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _OPENMP #include #endif #if defined(__F16C__) #include #endif // ==== Internals: model container, tensor access, kernel and pipeline declarations // 64-bit Arm with NEON: the kernels and the image and text stages take their Arm paths. #if defined(__aarch64__) && defined(__ARM_NEON) #include #define OASIS_NEON 1 #endif namespace oasis { class SpinPool; // Tensor types: 0 f32, 1 f16, 2 cq with fp16 group scales, 3 raw bytes, 4 signed integer rows of `bits` // bits with one f32 scale per row, 5 cq with 8-bit log-coded group scales. struct Entry { int dtype = 0; int bits = 0; std::vector shape; const uint8_t* data = nullptr; size_t nbytes = 0; }; // Quantised matrix [rows = outputs, cols = inputs]: packed indices, then one scale per row and 128-group, // stored either as fp16 (norms) or as an 8-bit code into a 256-entry table (ncode, nlut). struct CQ { int rows = 0, cols = 0, pc = 0, bits = 0; const uint8_t* idx = nullptr; const uint16_t* norms = nullptr; const uint8_t* ncode = nullptr; const float* nlut = nullptr; const float* cb = nullptr; const uint8_t* zero_rows = nullptr; // 1 for rows whose every group scale is zero; null when there are none }; bool inflate_exact(const uint8_t* in, size_t n, uint8_t* out, size_t raw); struct Model { std::vector file; std::vector> inflated; // tensors stored deflated, expanded at load std::unordered_map header; std::unordered_map entries; std::unordered_map> cache; // fp16 and integer-row tensors as f32, filled at load std::unordered_map> luts; // decoded 8-bit group-scale codes, filled at load std::unordered_map> zero_rows; // quantised matrices with all-zero rows std::unordered_map> dq; // dequantised matrices, filled on demand std::mutex dq_mutex; std::vector codebook; int cb_offset[5] = {0, 0, 0, 4, 12}; int vocab = 0, n_pieces = 0, bos = 2, eos = 1, pad = 0, ref_base = 0; std::unordered_map langs; // every language tag in the vocabulary std::vector languages; // the languages the model describes in std::vector pieces; std::vector piece_types; std::unordered_map piece_id; // normal pieces, for encoding std::vector> markers; // user-defined pieces, longest first int byte_id[256] = {0}; // byte-fallback pieces std::vector charset; std::vector perm1, perm2; std::vector seq_order; // sequency row -> natural row for the 512-point Walsh matrix std::shared_ptr pool; // decoder workers, created at the first decode std::mutex decode_mutex; bool load(const std::string& path, std::string& err); bool has(const std::string& name) const { return entries.count(name) != 0; } const Entry& E(const std::string& name) const; const float* F(const std::string& name); // f32 view, fp16 converted once CQ Q(const std::string& name) const; const float* Wdq(const std::string& name); // dequantised [rows, cols], cached }; // ---- scalar helpers // The exponent is capped so that no compiler's fast exp can overflow; below -80 the result is ~0 anyway. inline float sigmoidf(float x) { return 1.0f / (1.0f + std::exp(std::min(-x, 80.0f))); } inline float siluf(float x) { return x / (1.0f + std::exp(std::min(-x, 80.0f))); } inline float gelu_tanh(float x) { const float k = 0.7978845608028654f; return 0.5f * x * (1.0f + std::tanh(k * (x + 0.044715f * x * x * x))); } #if defined(OASIS_NEON) // expf as glibc 2.27 and later computes it on 64-bit Arm, four lanes at a time: x times 32/ln 2 rounded to // k with ties away from zero, 2^(k/32) from its table, and a cubic in the remainder, in double precision. // Equal to that library's expf for every float that is not a NaN. inline float32x2_t exp_lanes(float64x2_t xd) { static const uint64_t T[32] = { 0x3ff0000000000000, 0x3fefd9b0d3158574, 0x3fefb5586cf9890f, 0x3fef9301d0125b51, 0x3fef72b83c7d517b, 0x3fef54873168b9aa, 0x3fef387a6e756238, 0x3fef1e9df51fdee1, 0x3fef06fe0a31b715, 0x3feef1a7373aa9cb, 0x3feedea64c123422, 0x3feece086061892d, 0x3feebfdad5362a27, 0x3feeb42b569d4f82, 0x3feeab07dd485429, 0x3feea47eb03a5585, 0x3feea09e667f3bcd, 0x3fee9f75e8ec5f74, 0x3feea11473eb0187, 0x3feea589994cce13, 0x3feeace5422aa0db, 0x3feeb737b0cdc5e5, 0x3feec49182a3f090, 0x3feed503b23e255d, 0x3feee89f995ad3ad, 0x3feeff76f2fb5e47, 0x3fef199bdd85529c, 0x3fef3720dcef9069, 0x3fef5818dcfba487, 0x3fef7c97337b9b5f, 0x3fefa4afa2a490da, 0x3fefd0765b6e4540}; const float64x2_t z = vmulq_f64(vdupq_n_f64(0x1.71547652b82fep+0 * 32), xd); const float64x2_t r = vsubq_f64(z, vrndaq_f64(z)); const int64x2_t ki = vcvtaq_s64_f64(z); const uint64_t k0 = (uint64_t)vgetq_lane_s64(ki, 0), k1 = (uint64_t)vgetq_lane_s64(ki, 1); const float64x2_t s = vreinterpretq_f64_u64(vcombine_u64(vcreate_u64(T[k0 % 32] + (k0 << 47)), vcreate_u64(T[k1 % 32] + (k1 << 47)))); const float64x2_t c = vfmaq_f64(vdupq_n_f64(0x1.ebfce50fac4f3p-3 / 32 / 32), vdupq_n_f64(0x1.c6af84b912394p-5 / 32 / 32 / 32), r); const float64x2_t y = vfmaq_f64(vfmaq_f64(vdupq_n_f64(1.0), vdupq_n_f64(0x1.62e42ff0c52d6p-1 / 32), r), c, vmulq_f64(r, r)); return vcvt_f32_f64(vmulq_f64(y, s)); } inline float32x4_t expf4(float32x4_t x) { const float32x4_t lo = vdupq_n_f32(-0x1.9fe368p6f), hi = vdupq_n_f32(0x1.62e42ep6f); const float32x4_t xc = vminq_f32(vmaxq_f32(x, lo), hi); // NaNs pass through const float32x4_t e = vcombine_f32(exp_lanes(vcvt_f64_f32(vget_low_f32(xc))), exp_lanes(vcvt_high_f64_f32(xc))); const float32x4_t zero = vbslq_f32(vcltq_f32(x, lo), vdupq_n_f32(0.0f), e); return vbslq_f32(vcgtq_f32(x, hi), vdupq_n_f32(INFINITY), zero); } // tanhf as glibc 2.41 computes it (CORE-MATH's correctly rounded tanhf), four lanes at a time: a rational // function in double precision, with its own forms for tiny and large arguments. Equal to the library's // tanhf for every float. inline float32x2_t tanh_lanes(float64x2_t z) { static const double cn[] = {0x1p+0, 0x1.30877b8b72d33p-3, 0x1.694aa09ae9e5ep-8, 0x1.4101377abb729p-14, 0x1.e0392b1db0018p-22, 0x1.2533756e546f7p-30, 0x1.d62e5abe6ae8ap-41, 0x1.b06be534182dep-54}; static const double cd[] = {0x1p+0, 0x1.ed99131b0ebeap-2, 0x1.0d27ed6c95a69p-5, 0x1.7cbdaca0e9fccp-11, 0x1.b4e60b892578ep-18, 0x1.a6f707c5c71abp-26, 0x1.35a8b6e2cd94cp-35, 0x1.ca8230677aa01p-47}; const float64x2_t z2 = vmulq_f64(z, z), z4 = vmulq_f64(z2, z2), z8 = vmulq_f64(z4, z4); auto pair = [&](const double* c, int i) { return vfmaq_f64(vdupq_n_f64(c[i]), z2, vdupq_n_f64(c[i + 1])); }; float64x2_t n0 = pair(cn, 0), n2 = pair(cn, 2), n4 = pair(cn, 4), n6 = pair(cn, 6); n0 = vfmaq_f64(n0, z4, n2); n4 = vfmaq_f64(n4, z4, n6); n0 = vfmaq_f64(n0, z8, n4); float64x2_t d0 = pair(cd, 0), d2 = pair(cd, 2), d4 = pair(cd, 4), d6 = pair(cd, 6); d0 = vfmaq_f64(d0, z4, d2); d4 = vfmaq_f64(d4, z4, d6); d0 = vfmaq_f64(d0, z8, d4); return vcvt_f32_f64(vdivq_f64(vmulq_f64(z, n0), d0)); } inline float32x4_t tanhf4(float32x4_t x) { const uint32x4_t ax = vandq_u32(vreinterpretq_u32_f32(x), vdupq_n_u32(0x7fffffff)); float32x4_t r = vcombine_f32(tanh_lanes(vcvt_f64_f32(vget_low_f32(x))), tanh_lanes(vcvt_high_f64_f32(x))); const float32x4_t x2 = vmulq_f32(x, x); r = vbslq_f32(vcltq_u32(ax, vdupq_n_u32(115u << 23)), vfmaq_f32(x, x, vmulq_n_f32(x2, -0x1.555556p-2f)), r); r = vbslq_f32(vcltq_u32(ax, vdupq_n_u32(102u << 23)), vfmaq_f32(x, vnegq_f32(x), vabsq_f32(x)), r); r = vbslq_f32(vceqq_u32(ax, vdupq_n_u32(0)), x, r); const uint32x4_t sign = vandq_u32(vreinterpretq_u32_f32(x), vdupq_n_u32(0x80000000)); const float32x4_t one = vreinterpretq_f32_u32(vorrq_u32(sign, vreinterpretq_u32_f32(vdupq_n_f32(1.0f)))); const float32x4_t tiny = vreinterpretq_f32_u32(vorrq_u32(sign, vreinterpretq_u32_f32(vdupq_n_f32(0x1p-25f)))); r = vbslq_f32(vcgtq_u32(ax, vdupq_n_u32(0x41102cb3u)), vsubq_f32(one, tiny), r); return vbslq_f32(vcgtq_u32(ax, vdupq_n_u32(0x7f800000u)), vaddq_f32(x, x), r); } // o[i] *= sigmoidf(g[i]) and v[i] = siluf(v[i]) for n a multiple of 4, with the same roundings. inline void mul_sigmoid(float* o, const float* g, int n) { const float32x4_t cap = vdupq_n_f32(80.0f), one = vdupq_n_f32(1.0f); for (int i = 0; i < n; i += 4) { const float32x4_t e = expf4(vminq_f32(vnegq_f32(vld1q_f32(g + i)), cap)); vst1q_f32(o + i, vmulq_f32(vld1q_f32(o + i), vdivq_f32(one, vaddq_f32(one, e)))); } } inline void silu_inplace(float* v, int n) { const float32x4_t cap = vdupq_n_f32(80.0f), one = vdupq_n_f32(1.0f); for (int i = 0; i < n; i += 4) { const float32x4_t x = vld1q_f32(v + i); vst1q_f32(v + i, vdivq_f32(x, vaddq_f32(one, expf4(vminq_f32(vnegq_f32(x), cap))))); } } #endif float half_to_float(uint16_t h); extern bool g_exact_gemv; // float activations in quantised matrix-vector products (OASIS_EXACT=1) // Threads for one description: OMP_NUM_THREADS when it is set, otherwise every processor, less one on // machines with four or fewer, where the rest of the system would otherwise preempt a worker at every step. int engine_threads(); // Scale of group i (row-major over rows and 128-groups) of a quantised matrix. inline float cq_scale(const CQ& q, size_t i) { return q.ncode ? q.nlut[q.ncode[i]] : half_to_float(q.norms[i]); } // Activations prepared once for several quantised matrix-vector products with the same input: // Walsh-rotated per 128-group, int8 per group, reordered to the packed-index layout. struct QX { std::vector xs, xa; std::vector sx; int pc = 0, bits = 0; }; // Codebook entries as int8 with one scale, shared by every row of a matrix of the same width. struct CbTable { alignas(16) int8_t tbl[16]; float cs; }; // ---- kernels void prep_x(const float* x, int cols, int bits, QX& out); CbTable cb_table(const CQ& q); float cq_row(const CQ& q, const QX& qx, int r, const CbTable& t); // one output of W x void cq_gemv_q(const CQ& q, const QX& qx, float* y); void cq_gemm_q(const CQ& q, const float* A, int M, float* C); // int8-activation matrix product struct Model; void qmm(Model& m, const std::string& name, const float* A, int M, float* C); // C = A W^T for a quantised W float dotf(const float* a, const float* b, int n); void wht_inplace(float* x, int n); // natural order, normalised void wht_seq512(const Model& m, const float* x, float* out); // sequency-ordered 512-point transform void cq_dequant_row(const CQ& q, int row, float* out); void cq_gemv(const CQ& q, const float* x, float* y); // y[rows] = W x void gemm_nt(const float* A, int M, int K, const float* W, int N, float* C); // C = A W^T, W [N, K] void gemm_nn(const float* A, int M, int K, const float* B, int N, float* C); // C = A B, B [K, N] void zc_norm(const float* x, int n, const float* scale, float* out); void rms_unit(const float* x, int n, float* out); void band65(const float* u, const float* w, int n, float* out); // per-output 65-tap band, zero edges // ---- pipeline struct Box { float x0, y0, x1, y1; // canvas-normalised [0, 1] float score; int ci, cj; // centre cell on the 64 x 64 grid }; struct Meta { int W = 0, H = 0, w = 0, h = 0; float r = 1.0f; }; struct Encoded { Meta meta; std::vector E; // [256, 512] std::vector D; // [64, 64, 128] std::vector S4; // [128, 128, 64] std::vector valid32, valid16, valid8, valid4; std::vector det; // [64, 64, 10] }; // One field of a form request, in request order. struct FormField { enum Kind { CHOICE, COUNT, TEXT, BOX } kind = CHOICE; std::string name; std::vector choices; // CHOICE: candidate texts std::vector values; // CHOICE: each choice as JSON (string or number) long lo = 0, hi = 0; // COUNT: inclusive range int max_length = 60; // TEXT std::string of; // BOX: the earlier choice field it locates }; struct Options { std::string language = "en"; float conf = 0.3f, text_conf = 0.35f, mask_threshold = 0.5f; int max_objects = 20, max_text_lines = 20, max_tokens = 512; std::string mask_format = "largest"; bool masks = true, nms = true, reasoning = true; std::vector sections, fields, keywords; std::vector form; }; Encoded encode(Model& m, const uint8_t* rgb, int W, int H); void detect(const Encoded& enc, float conf, float text_conf, bool nms, int max_obj, int max_txt, std::vector& objects, std::vector& texts); std::vector mask_prob(Model& m, const Encoded& enc, const Box& b, std::vector* protos); std::string read_text(Model& m, const uint8_t* rgb, int W, int H, float x0, float y0, float x1, float y1); void memory(Model& m, const Encoded& enc, const std::vector& objects, const std::vector& texts, std::vector& M, std::vector& mvalid, std::vector& refs); // keywords: words and phrases whose tokens are favoured while the description is decoded. std::vector generate(Model& m, const std::vector& M, const std::vector& mvalid, const std::vector& refs, const std::string& language, int max_tokens, const std::vector& keywords = {}); std::string detokenize(const Model& m, const std::vector& ids); std::vector encode_text(const Model& m, const std::string& text); // SentencePiece BPE, no dummy prefix // Fills a form against the decoder memory; ref_boxes are the 0-1000 boxes of refs. Returns the "form" object. std::string fill_form(Model& m, const std::vector& M, const std::vector& mvalid, const std::vector& refs, const std::vector>& ref_boxes, const std::string& language, const std::vector& form); std::string describe_json(Model& m, const uint8_t* rgb, int W, int H, const Options& o, double* timings); } // namespace oasis // ==== Worker threads that stay awake for the length of one decode // // Each decoded token runs dozens of parallel steps that last a few microseconds. Workers that an // OpenMP runtime parks between such steps take longer to wake than the steps take to run, so the // decoder shares its work through this pool instead: while a decode is in progress the workers spin // on a generation counter, and outside it they sleep on a condition variable. #if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) #include #define OASIS_PAUSE() _mm_pause() #elif defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) #define OASIS_PAUSE() __asm__ __volatile__("yield" ::: "memory") #else #define OASIS_PAUSE() std::this_thread::yield() #endif namespace oasis { class SpinPool { public: explicit SpinPool(int threads) : n_(std::max(1, threads)) { for (int i = 1; i < n_; i++) workers_.emplace_back([this] { loop(); }); } ~SpinPool() { { std::lock_guard lock(mutex_); stop_.store(true); awake_.store(true); } cv_.notify_all(); for (auto& t : workers_) t.join(); } SpinPool(const SpinPool&) = delete; SpinPool& operator=(const SpinPool&) = delete; int size() const { return n_; } void wake() { { std::lock_guard lock(mutex_); awake_.store(true); } cv_.notify_all(); } // Workers notice within a few hundred spins and go back to sleep. void sleep() { std::lock_guard lock(mutex_); awake_.store(false); } // Calls f(i) once for every i in [0, n). Indices are claimed in chunks by whichever thread is free, // and each is computed entirely by one thread, so results do not depend on the thread count. The call // returns once every index is done: a worker the system has not scheduled meanwhile takes no part in // the step instead of holding the others up. template void run(int n, int chunk, F&& f) { if (n <= 0) return; if (n_ == 1 || n <= chunk) { for (int i = 0; i < n; i++) f(i); return; } if (!awake_.load(std::memory_order_relaxed)) wake(); using G = typename std::remove_reference::type; const uint64_t gen = (ticket_.load(std::memory_order_relaxed) >> 32) + 1; ticket_.store(gen << 32 | CLOSED, std::memory_order_relaxed); fn_.store([](void* ctx, int b, int e) { G& g = *static_cast(ctx); for (int i = b; i < e; i++) g(i); }, std::memory_order_relaxed); ctx_.store((void*)&f, std::memory_order_relaxed); chunk_.store(std::max(1, chunk), std::memory_order_relaxed); done_.store(0, std::memory_order_relaxed); items_.store(n, std::memory_order_release); ticket_.store(gen << 32, std::memory_order_release); work(gen); while (done_.load(std::memory_order_acquire) < n) OASIS_PAUSE(); } private: // The ticket holds the step's generation in its high half and the next unclaimed index in its low half. // run() closes the new generation before it writes the step and publishes the step's size after that, so // a worker holding a ticket of an earlier step fails its claim: either it reads that step's size, which // its index has passed, or it reads the new size, and its claim then meets the closed ticket. A step // cannot finish while a claimed chunk is outstanding, so the step's function and context stay valid for // every successful claim. static constexpr uint32_t CLOSED = 0xffffffffu; void work(uint64_t gen) { for (;;) { uint64_t t = ticket_.load(std::memory_order_acquire); if ((t >> 32) != gen) return; if ((uint32_t)t == CLOSED) { OASIS_PAUSE(); continue; } const int items = items_.load(std::memory_order_acquire), chunk = chunk_.load(std::memory_order_relaxed); const int b = (int)(uint32_t)t; if (b >= items) return; if (!ticket_.compare_exchange_weak(t, t + (uint64_t)chunk, std::memory_order_acq_rel, std::memory_order_relaxed)) continue; const int e = std::min(items, b + chunk); fn_.load(std::memory_order_relaxed)(ctx_.load(std::memory_order_relaxed), b, e); done_.fetch_add(e - b, std::memory_order_release); } } void loop() { uint64_t seen = ticket_.load(std::memory_order_acquire) >> 32; for (;;) { uint64_t g; int spins = 0; while ((g = ticket_.load(std::memory_order_acquire) >> 32) == seen) { if (stop_.load(std::memory_order_relaxed)) return; if (++spins >= 256) { spins = 0; if (!awake_.load(std::memory_order_relaxed)) { std::unique_lock lock(mutex_); cv_.wait(lock, [&] { return awake_.load() || stop_.load() || (ticket_.load(std::memory_order_acquire) >> 32) != seen; }); } } OASIS_PAUSE(); } seen = g; work(g); } } int n_; std::vector workers_; std::mutex mutex_; std::condition_variable cv_; std::atomic awake_{false}, stop_{false}; std::atomic ticket_{0}; std::atomic items_{0}, chunk_{1}, done_{0}; std::atomic fn_{nullptr}; std::atomic ctx_{nullptr}; }; } // namespace oasis // ==== zlib inflate (RFC 1950 and 1951), for the tensors stored deflated namespace oasis { namespace { // Canonical Huffman code. Codes of up to 9 bits resolve in one lookup of the next 9 input bits, longer // ones by walking the code lengths. struct Huffman { uint16_t fast[512]; // length << 9 | symbol, or 0 when the next 9 bits start no code of up to 9 bits uint16_t count[16]; // codes of each length uint16_t symbol[288]; // symbols in code order bool build(const uint8_t* lengths, int n) { std::memset(count, 0, sizeof count); for (int i = 0; i < n; i++) count[lengths[i]]++; count[0] = 0; int left = 1; for (int l = 1; l < 16; l++) { left = 2 * left - count[l]; if (left < 0) return false; // over-subscribed } uint16_t next[16] = {0}; for (int l = 1; l < 15; l++) next[l + 1] = (uint16_t)(next[l] + count[l]); for (int i = 0; i < n; i++) if (lengths[i]) symbol[next[lengths[i]]++] = (uint16_t)i; std::memset(fast, 0, sizeof fast); for (int l = 1, code = 0, k = 0; l <= 9; l++, code <<= 1) for (int j = 0; j < count[l]; j++, code++, k++) { int rev = 0; // deflate sends a code's bits from its most significant one for (int b = 0; b < l; b++) rev |= ((code >> b) & 1) << (l - 1 - b); for (int f = rev; f < 512; f += 1 << l) fast[f] = (uint16_t)(l << 9 | symbol[k]); } return true; } }; // Bits in stream order, least significant first. Past the end it supplies zero bytes and counts them, so // that a stream which ends early is caught by overrun(). struct BitReader { const uint8_t* p; const uint8_t* end; uint64_t buf = 0; int cnt = 0; size_t past = 0; void fill() { while (cnt <= 56) { uint64_t byte = 0; if (p < end) byte = *p++; else past++; buf |= byte << cnt; cnt += 8; } } uint32_t bits(int n) { if (cnt < n) fill(); uint32_t v = (uint32_t)(buf & ((1ull << n) - 1)); buf >>= n; cnt -= n; return v; } bool overrun() const { return past * 8 > (size_t)cnt; } }; int decode(BitReader& br, const Huffman& h) { if (br.cnt < 16) br.fill(); int e = h.fast[br.buf & 511]; if (e) { br.buf >>= e >> 9; br.cnt -= e >> 9; return e & 511; } int code = 0, first = 0, index = 0; for (int l = 1; l < 16; l++) { code |= (int)(br.buf & 1); br.buf >>= 1; br.cnt--; int c = h.count[l]; if (code - first < c) return h.symbol[index + code - first]; index += c; first = (first + c) << 1; code <<= 1; } return -1; } // The literal/length and distance codes of a block with dynamic Huffman codes. bool read_tables(BitReader& br, Huffman& lit, Huffman& dist) { static const uint8_t ORDER[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; int nlit = (int)br.bits(5) + 257, ndist = (int)br.bits(5) + 1, ncode = (int)br.bits(4) + 4; if (nlit > 286 || ndist > 30) return false; uint8_t cl[19] = {0}; for (int i = 0; i < ncode; i++) cl[ORDER[i]] = (uint8_t)br.bits(3); Huffman code; if (!code.build(cl, 19)) return false; uint8_t lengths[286 + 30]; for (int k = 0; k < nlit + ndist;) { int s = decode(br, code); if (s < 0) return false; if (s < 16) { lengths[k++] = (uint8_t)s; continue; } int rep, value = 0; if (s == 16) { if (k == 0) return false; value = lengths[k - 1]; rep = 3 + (int)br.bits(2); } else if (s == 17) { rep = 3 + (int)br.bits(3); } else { rep = 11 + (int)br.bits(7); } if (k + rep > nlit + ndist) return false; while (rep--) lengths[k++] = (uint8_t)value; } return lengths[256] != 0 && lit.build(lengths, nlit) && dist.build(lengths + nlit, ndist); } const uint16_t LEN_BASE[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258}; const uint8_t LEN_EXTRA[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}; const uint16_t DIST_BASE[30] = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577}; const uint8_t DIST_EXTRA[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; uint32_t adler32(const uint8_t* p, size_t n) { uint32_t a = 1, b = 0; while (n) { size_t k = std::min(n, 5552); // the most bytes before b can overflow n -= k; while (k--) { a += *p++; b += a; } a %= 65521; b %= 65521; } return b << 16 | a; } } // namespace // Inflates a zlib stream into exactly raw bytes; false when the stream is damaged, fails its checksum or // holds another length. bool inflate_exact(const uint8_t* in, size_t n, uint8_t* out, size_t raw) { if (n < 6 || (in[0] & 15) != 8 || (in[0] >> 4) > 7 || ((in[0] << 8) | in[1]) % 31 || (in[1] & 32)) return false; static const std::pair fixed = [] { uint8_t l[288 + 30]; for (int i = 0; i < 288; i++) l[i] = i < 144 ? 8 : i < 256 ? 9 : i < 280 ? 7 : 8; for (int i = 0; i < 30; i++) l[288 + i] = 5; std::pair t; t.first.build(l, 288); t.second.build(l + 288, 30); return t; }(); BitReader br{in + 2, in + n - 4}; Huffman lit, dist; size_t o = 0; for (bool last = false; !last;) { last = br.bits(1) != 0; int type = (int)br.bits(2); if (type == 0) { int drop = br.cnt & 7; // stored blocks start on a byte boundary br.buf >>= drop; br.cnt -= drop; uint32_t len = br.bits(16), nlen = br.bits(16); if ((len ^ 0xffff) != nlen || len > raw - o) return false; for (uint32_t i = 0; i < len; i++) out[o++] = (uint8_t)br.bits(8); } else if (type == 1 || type == 2) { const Huffman* L = &fixed.first; const Huffman* D = &fixed.second; if (type == 2) { if (!read_tables(br, lit, dist)) return false; L = &lit; D = &dist; } for (;;) { int s = decode(br, *L); if (s < 256) { if (s < 0 || o == raw) return false; out[o++] = (uint8_t)s; } else if (s == 256) { break; } else { s -= 257; if (s >= 29) return false; size_t len = LEN_BASE[s] + br.bits(LEN_EXTRA[s]); int d = decode(br, *D); if (d < 0 || d >= 30) return false; size_t back = DIST_BASE[d] + br.bits(DIST_EXTRA[d]); if (back > o || len > raw - o) return false; for (size_t i = 0; i < len; i++, o++) out[o] = out[o - back]; } } } else { return false; } if (br.overrun()) return false; } const uint8_t* t = in + n - 4; uint32_t want = (uint32_t)t[0] << 24 | (uint32_t)t[1] << 16 | (uint32_t)t[2] << 8 | t[3]; return o == raw && adler32(out, raw) == want; } } // namespace oasis // ==== .oasis container loading and tensor access namespace oasis { float half_to_float(uint16_t h) { #if defined(__F16C__) return _cvtsh_ss(h); #elif defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) __fp16 v; std::memcpy(&v, &h, 2); return (float)v; #else uint32_t sign = (uint32_t)(h & 0x8000) << 16; uint32_t exp = (h >> 10) & 0x1f; uint32_t man = h & 0x3ff; uint32_t bits; if (exp == 0) { if (man == 0) { bits = sign; } else { exp = 127 - 15 + 1; while (!(man & 0x400)) { man <<= 1; exp--; } man &= 0x3ff; bits = sign | (exp << 23) | (man << 13); } } else if (exp == 31) { bits = sign | 0x7f800000 | (man << 13); } else { bits = sign | ((exp + 127 - 15) << 23) | (man << 13); } float f; std::memcpy(&f, &bits, 4); return f; #endif } template static T rd(const uint8_t*& p) { T v; std::memcpy(&v, p, sizeof(T)); p += sizeof(T); return v; } bool Model::load(const std::string& path, std::string& err) { std::ifstream f(path, std::ios::binary); if (!f) { err = "cannot open " + path; return false; } file.assign(std::istreambuf_iterator(f), std::istreambuf_iterator()); if (file.size() < 16 || std::memcmp(file.data(), "OASIS\0v1", 8) != 0) { err = "not an .oasis v1 file"; return false; } const uint8_t* p = file.data() + 8; uint32_t hl = rd(p); std::string head((const char*)p, hl); p += hl; std::istringstream hs(head); std::string line; while (std::getline(hs, line)) { auto sp = line.find(' '); if (sp != std::string::npos) header[line.substr(0, sp)] = line.substr(sp + 1); } uint32_t n = rd(p); struct Raw { std::string name; Entry e; uint64_t off; int flags; }; std::vector raws; for (uint32_t i = 0; i < n; i++) { uint16_t nl = rd(p); Raw r; r.name.assign((const char*)p, nl); p += nl; r.e.dtype = rd(p); r.e.bits = rd(p); int nd = rd(p); r.flags = rd(p); for (int d = 0; d < nd; d++) r.e.shape.push_back((int)rd(p)); r.off = rd(p); r.e.nbytes = (size_t)rd(p); raws.push_back(std::move(r)); } size_t data_start = ((size_t)(p - file.data()) + 63) / 64 * 64; for (auto& r : raws) { if (data_start + r.off + r.e.nbytes > file.size()) { err = "truncated tensor " + r.name; return false; } r.e.data = file.data() + data_start + r.off; if (r.flags & 1) { // Deflated tensor: u64 size of the stored bytes, then a zlib stream of them. if (r.e.nbytes < 8) { err = "bad compressed tensor " + r.name; return false; } uint64_t raw; std::memcpy(&raw, r.e.data, 8); inflated.emplace_back(raw); if (!inflate_exact(r.e.data + 8, r.e.nbytes - 8, inflated.back().data(), raw)) { err = "cannot inflate tensor " + r.name; return false; } r.e.data = inflated.back().data(); r.e.nbytes = raw; } entries[r.name] = r.e; } auto num = [&](const char* k) { return std::stoi(header.at(k)); }; vocab = num("vocab"); n_pieces = num("n_pieces"); bos = num("bos"); eos = num("eos"); pad = num("pad"); ref_base = num("ref_base"); std::istringstream ls(header.at("langs")); std::string tokn; std::vector all; while (ls >> tokn) { auto c = tokn.find(':'); langs[tokn.substr(0, c)] = std::stoi(tokn.substr(c + 1)); all.push_back(tokn.substr(0, c)); } auto lang_line = header.find("languages"); if (lang_line != header.end()) { std::istringstream as(lang_line->second); while (as >> tokn) if (langs.count(tokn)) languages.push_back(tokn); } if (languages.empty()) languages = all; const float* cbp = (const float*)E("quant/codebook").data; codebook.assign(cbp, cbp + E("quant/codebook").shape[0]); const Entry& pe = E("tokenizer/pieces"); const uint8_t* q = pe.data; uint32_t np = rd(q); for (uint32_t i = 0; i < np; i++) { int t = rd(q); uint16_t l = rd(q); pieces.emplace_back((const char*)q, l); piece_types.push_back(t); q += l; const std::string& p = pieces.back(); if (t == 0) piece_id.emplace(p, (int)i); else if (t == 3 && !p.empty()) markers.push_back({p, (int)i}); else if (t == 4 && p.size() == 6 && p.compare(0, 3, "<0x") == 0) byte_id[std::stoi(p.substr(3, 2), nullptr, 16) & 255] = (int)i; } std::stable_sort(markers.begin(), markers.end(), [](const auto& a, const auto& b) { return a.first.size() > b.first.size(); }); const Entry& ce = E("reader/charset"); for (int i = 0; i < ce.shape[0]; i++) charset.push_back((uint32_t)((const float*)ce.data)[i]); for (int i = 0; i < 512; i++) { perm1.push_back((int)((const float*)E("hmlp/perm1").data)[i]); perm2.push_back((int)((const float*)E("hmlp/perm2").data)[i]); } seq_order.resize(512); for (int j = 0; j < 512; j++) { int g = j ^ (j >> 1), s = 0; for (int b = 0; b < 9; b++) if (g & (1 << b)) s |= 1 << (8 - b); seq_order[j] = s; } // Expand every fp16 and integer-row tensor now, and decode every 8-bit scale table, so that lookups // during inference never write to the model. for (auto& kv : entries) { const Entry& e = kv.second; if (e.dtype == 1) { size_t n = e.nbytes / 2; std::vector v(n); const uint16_t* h = (const uint16_t*)e.data; for (size_t i = 0; i < n; i++) v[i] = half_to_float(h[i]); cache[kv.first] = std::move(v); } else if (e.dtype == 4) { // Signed integers of e.bits bits in one LSB-first bit stream, then one f32 scale per row. int rows = e.shape[0], b = e.bits; size_t cols = 1; for (size_t d = 1; d < e.shape.size(); d++) cols *= (size_t)e.shape[d]; size_t n = (size_t)rows * cols, packed = (n * b + 7) / 8; if (b < 2 || b > 16 || e.nbytes != packed + (size_t)rows * 4) { err = "bad integer tensor " + kv.first; return false; } std::vector v(n); for (int r = 0; r < rows; r++) { float s; std::memcpy(&s, e.data + packed + (size_t)r * 4, 4); for (size_t c = 0; c < cols; c++) { size_t bit = ((size_t)r * cols + c) * b; uint32_t w = 0; for (int k = 0; k < b; k++, bit++) w |= (uint32_t)(e.data[bit >> 3] >> (bit & 7) & 1) << k; int32_t x = (int32_t)(w << (32 - b)) >> (32 - b); v[(size_t)r * cols + c] = s * (float)x; } } cache[kv.first] = std::move(v); } else if (e.dtype == 5) { // Code 0 is a zero scale; codes 1..255 step evenly in log scale from exp(lo) to exp(hi). float lo, hi; std::memcpy(&lo, e.data + e.nbytes - 8, 4); std::memcpy(&hi, e.data + e.nbytes - 4, 4); std::vector lut(256, 0.0f); for (int c = 1; c < 256; c++) lut[c] = std::exp(lo + (hi - lo) * (float)(c - 1) / 254.0f); luts[kv.first] = std::move(lut); } if (e.dtype == 2 || e.dtype == 5) { // A row whose every group scale is zero multiplies to zero; the kernels skip it. int rows = e.shape[0]; for (size_t d = 1; d + 1 < e.shape.size(); d++) rows *= e.shape[d]; const int groups = (e.shape.back() + 127) / 128; const uint8_t* sc = e.data + (size_t)rows * groups * 128 * e.bits / 8; std::vector z(rows, 1); bool any = false; for (int r = 0; r < rows; r++) { for (int g = 0; g < groups && z[r]; g++) { size_t i = (size_t)r * groups + g; z[r] = e.dtype == 5 ? sc[i] == 0 : (((const uint16_t*)sc)[i] & 0x7fff) == 0; } any = any || z[r]; } if (any) zero_rows[kv.first] = std::move(z); } } #if defined(OASIS_NEON) // On Arm the HConvGLU bands are kept tap-major ([65, rows]), the order the band kernel reads them in. for (auto& kv : cache) { const std::string& k = kv.first; auto ends = [&](const char* s) { size_t n = std::strlen(s); return k.size() >= n && k.compare(k.size() - n, n, s) == 0; }; if (!ends("hconv/gate_conv") && !ends("hconv/value_conv") && !ends("hconv/out_conv")) continue; const Entry& e = entries.at(k); if (e.shape.size() != 2 || e.shape[0] != 512 || e.shape[1] != 65) continue; std::vector t((size_t)65 * 512); for (int r = 0; r < 512; r++) for (int j = 0; j < 65; j++) t[(size_t)j * 512 + r] = kv.second[(size_t)r * 65 + j]; kv.second.swap(t); } #endif return true; } const Entry& Model::E(const std::string& name) const { auto it = entries.find(name); if (it == entries.end()) throw std::runtime_error("missing tensor " + name); return it->second; } const float* Model::F(const std::string& name) { const Entry& e = E(name); if (e.dtype == 0) return (const float*)e.data; if (e.dtype != 1 && e.dtype != 4) throw std::runtime_error("tensor is not a float array: " + name); return cache.at(name).data(); } CQ Model::Q(const std::string& name) const { const Entry& e = E(name); if (e.dtype != 2 && e.dtype != 5) throw std::runtime_error("tensor is not quantised: " + name); CQ q; q.rows = e.shape[0]; for (size_t d = 1; d + 1 < e.shape.size(); d++) q.rows *= e.shape[d]; q.cols = e.shape.back(); q.pc = (q.cols + 127) / 128 * 128; q.bits = e.bits; q.idx = e.data; const uint8_t* scales = e.data + (size_t)q.rows * q.pc * q.bits / 8; if (e.dtype == 5) { q.ncode = scales; q.nlut = luts.at(name).data(); } else { q.norms = (const uint16_t*)scales; } q.cb = codebook.data() + cb_offset[q.bits]; auto zr = zero_rows.find(name); if (zr != zero_rows.end()) q.zero_rows = zr->second.data(); return q; } const float* Model::Wdq(const std::string& name) { std::lock_guard lock(dq_mutex); std::string key = "#dq:" + name; auto c = dq.find(key); if (c != dq.end()) return c->second.data(); CQ q = Q(name); std::vector v((size_t)q.rows * q.cols); for (int r = 0; r < q.rows; r++) cq_dequant_row(q, r, v.data() + (size_t)r * q.cols); return (dq[key] = std::move(v)).data(); } } // namespace oasis // ==== Numeric kernels: Walsh-Hadamard transforms, codebook matrices, GEMM, norms and bands namespace oasis { int engine_threads() { static const int n = [] { #ifdef _OPENMP const char* e = std::getenv("OMP_NUM_THREADS"); if (e && *e) return std::max(1, omp_get_max_threads()); const int p = omp_get_num_procs(); #else const int p = (int)std::thread::hardware_concurrency(); #endif return p <= 4 ? std::max(1, p - 1) : p; }(); return n; } void wht_inplace(float* x, int n) { #if defined(OASIS_NEON) // The same sums and differences on every element as the loops below, four elements at a time. if (n >= 8) { for (int i = 0; i < n; i += 8) { // pairs one apart const float32x4_t a = vld1q_f32(x + i), b = vld1q_f32(x + i + 4); const float32x4_t e = vuzp1q_f32(a, b), o = vuzp2q_f32(a, b); const float32x4_t s = vaddq_f32(e, o), d = vsubq_f32(e, o); vst1q_f32(x + i, vzip1q_f32(s, d)); vst1q_f32(x + i + 4, vzip2q_f32(s, d)); } for (int i = 0; i < n; i += 8) { // pairs two apart const float64x2_t a = vreinterpretq_f64_f32(vld1q_f32(x + i)), b = vreinterpretq_f64_f32(vld1q_f32(x + i + 4)); const float32x4_t lo = vreinterpretq_f32_f64(vzip1q_f64(a, b)), hi = vreinterpretq_f32_f64(vzip2q_f64(a, b)); const float64x2_t s = vreinterpretq_f64_f32(vaddq_f32(lo, hi)), d = vreinterpretq_f64_f32(vsubq_f32(lo, hi)); vst1q_f32(x + i, vreinterpretq_f32_f64(vzip1q_f64(s, d))); vst1q_f32(x + i + 4, vreinterpretq_f32_f64(vzip2q_f64(s, d))); } for (int len = 4; len < n; len <<= 1) for (int i = 0; i < n; i += len << 1) for (int j = i; j < i + len; j += 4) { const float32x4_t a = vld1q_f32(x + j), b = vld1q_f32(x + j + len); vst1q_f32(x + j, vaddq_f32(a, b)); vst1q_f32(x + j + len, vsubq_f32(a, b)); } const float s = 1.0f / std::sqrt((float)n); for (int i = 0; i < n; i += 4) vst1q_f32(x + i, vmulq_n_f32(vld1q_f32(x + i), s)); return; } #endif for (int len = 1; len < n; len <<= 1) for (int i = 0; i < n; i += len << 1) for (int j = i; j < i + len; j++) { float a = x[j], b = x[j + len]; x[j] = a + b; x[j + len] = a - b; } float s = 1.0f / std::sqrt((float)n); for (int i = 0; i < n; i++) x[i] *= s; } void wht_seq512(const Model& m, const float* x, float* out) { float t[512]; std::memcpy(t, x, sizeof t); wht_inplace(t, 512); for (int j = 0; j < 512; j++) out[j] = t[m.seq_order[j]]; } static inline int cq_index(const uint8_t* base, size_t pos, int bits) { size_t bit = pos * bits; size_t byte = bit >> 3; int shift = (int)(bit & 7); unsigned v = base[byte] >> shift; if (shift + bits > 8) v |= (unsigned)base[byte + 1] << (8 - shift); return (int)(v & ((1u << bits) - 1)); } void cq_dequant_row(const CQ& q, int row, float* out) { int groups = q.pc / 128; float g[128]; for (int gi = 0; gi < groups; gi++) { float s = cq_scale(q, (size_t)row * groups + gi); size_t base = (size_t)row * q.pc + (size_t)gi * 128; for (int i = 0; i < 128; i++) g[i] = q.cb[cq_index(q.idx, base + i, q.bits)] * s; wht_inplace(g, 128); int n = std::min(128, q.cols - gi * 128); if (n > 0) std::memcpy(out + gi * 128, g, sizeof(float) * n); } } #if defined(__AVX2__) #include // Every AVX2 processor also has FMA3; MSVC enables both with /arch:AVX2 but defines no __FMA__. #if defined(__FMA__) || defined(_MSC_VER) #define OASIS_FMA 1 #endif static inline __m256i dot_i8(__m256i acc, __m256i ax, __m256i ws) { #if defined(__AVXVNNI__) return _mm256_dpbusd_avx_epi32(acc, ax, ws); #else return _mm256_add_epi32(acc, _mm256_madd_epi16(_mm256_maddubs_epi16(ax, ws), _mm256_set1_epi16(1))); #endif } static inline int hsum_i32(__m256i v) { __m128i s = _mm_add_epi32(_mm256_castsi256_si128(v), _mm256_extracti128_si256(v, 1)); s = _mm_add_epi32(s, _mm_shuffle_epi32(s, 0x4E)); s = _mm_add_epi32(s, _mm_shuffle_epi32(s, 0xB1)); return _mm_cvtsi128_si32(s); } #elif defined(OASIS_NEON) // Signed int8 products of a and b added in fours into the int32 lanes of acc: SDOT where the core has the // dot-product instructions, otherwise widening multiplies with pairwise accumulation. Both are exact. static inline int32x4_t dot_i8(int32x4_t acc, int8x16_t a, int8x16_t b) { #if defined(__ARM_FEATURE_DOTPROD) return vdotq_s32(acc, a, b); #else acc = vpadalq_s16(acc, vmull_s8(vget_low_s8(a), vget_low_s8(b))); return vpadalq_s16(acc, vmull_high_s8(a, b)); #endif } #endif bool g_exact_gemv = false; void prep_x(const float* x, int cols, int bits, QX& o) { o.pc = (cols + 127) / 128 * 128; o.bits = bits; int groups = o.pc / 128; float xt[2048]; std::memset(xt, 0, sizeof(float) * o.pc); std::memcpy(xt, x, sizeof(float) * cols); o.xs.resize(o.pc); o.xa.resize(o.pc); o.sx.resize(groups); for (int g = 0; g < groups; g++) { float* v = xt + g * 128; wht_inplace(v, 128); #if defined(OASIS_NEON) int8_t* dst = o.xs.data() + g * 128; // The same scale, roundings and reordering as the loops below, sixteen values at a time. float32x4_t mx = vdupq_n_f32(0.0f); for (int i = 0; i < 128; i += 4) mx = vmaxnmq_f32(mx, vabsq_f32(vld1q_f32(v + i))); const float amax = vmaxnmvq_f32(mx); const float inv = amax > 0 ? 127.0f / amax : 0.0f; o.sx[g] = amax / 127.0f; int8_t tmp[128]; for (int i = 0; i < 128; i += 16) { int32x4_t q[4]; for (int k = 0; k < 4; k++) q[k] = vcvtnq_s32_f32(vmulq_n_f32(vld1q_f32(v + i + 4 * k), inv)); const int16x8_t h0 = vcombine_s16(vmovn_s32(q[0]), vmovn_s32(q[1])), h1 = vcombine_s16(vmovn_s32(q[2]), vmovn_s32(q[3])); vst1q_s8(tmp + i, vcombine_s8(vmovn_s16(h0), vmovn_s16(h1))); } if (bits == 2) { const int8x16x4_t a = vld4q_s8(tmp), b = vld4q_s8(tmp + 64); for (int k = 0; k < 4; k++) { vst1q_s8(dst + k * 32, a.val[k]); vst1q_s8(dst + k * 32 + 16, b.val[k]); } } else { for (int c = 0; c < 2; c++) { const int8x16x2_t a = vld2q_s8(tmp + 64 * c), b = vld2q_s8(tmp + 64 * c + 32); vst1q_s8(dst + c * 64, a.val[0]); vst1q_s8(dst + c * 64 + 16, b.val[0]); vst1q_s8(dst + c * 64 + 32, a.val[1]); vst1q_s8(dst + c * 64 + 48, b.val[1]); } } for (int i = 0; i < 128; i += 16) vst1q_s8(o.xa.data() + g * 128 + i, vabsq_s8(vld1q_s8(dst + i))); #else float amax = 0.0f; for (int i = 0; i < 128; i++) amax = std::max(amax, std::fabs(v[i])); float inv = amax > 0 ? 127.0f / amax : 0.0f; o.sx[g] = amax / 127.0f; int8_t tmp[128]; for (int i = 0; i < 128; i++) tmp[i] = (int8_t)std::nearbyint(v[i] * inv); int8_t* dst = o.xs.data() + g * 128; if (bits == 2) { for (int k = 0; k < 4; k++) for (int mm = 0; mm < 32; mm++) dst[k * 32 + mm] = tmp[4 * mm + k]; } else { for (int c = 0; c < 2; c++) for (int mm = 0; mm < 32; mm++) { dst[c * 64 + mm] = tmp[2 * (32 * c + mm)]; dst[c * 64 + 32 + mm] = tmp[2 * (32 * c + mm) + 1]; } } for (int i = 0; i < 128; i++) o.xa[g * 128 + i] = (int8_t)std::abs((int)dst[i]); #endif } } CbTable cb_table(const CQ& q) { CbTable t; int ncb = 1 << q.bits; float cmax = 0.0f; for (int i = 0; i < ncb; i++) cmax = std::max(cmax, std::fabs(q.cb[i])); t.cs = cmax / 127.0f; std::memset(t.tbl, 0, sizeof t.tbl); for (int i = 0; i < ncb; i++) t.tbl[i] = (int8_t)std::nearbyint(q.cb[i] / t.cs); return t; } // One output of W x for a quantised row: int8 dot products per rotated group, scaled per group. float cq_row(const CQ& q, const QX& qx, int r, const CbTable& t) { int groups = q.pc / 128; size_t row_bytes = (size_t)q.pc * q.bits / 8; const uint8_t* ib = q.idx + (size_t)r * row_bytes; float acc = 0.0f; #if defined(__AVX2__) const __m256i T = _mm256_broadcastsi128_si256(_mm_load_si128((const __m128i*)t.tbl)); const __m256i m3 = _mm256_set1_epi8(3), m15 = _mm256_set1_epi8(15); for (int g = 0; g < groups; g++) { const int8_t* xg = qx.xs.data() + g * 128; const int8_t* ag = qx.xa.data() + g * 128; __m256i a = _mm256_setzero_si256(); if (q.bits == 2) { __m256i b = _mm256_loadu_si256((const __m256i*)(ib + g * 32)); for (int k = 0; k < 4; k++) { __m256i w = _mm256_shuffle_epi8(T, _mm256_and_si256(_mm256_srli_epi16(b, 2 * k), m3)); a = dot_i8(a, _mm256_loadu_si256((const __m256i*)(ag + k * 32)), _mm256_sign_epi8(w, _mm256_loadu_si256((const __m256i*)(xg + k * 32)))); } } else { for (int c = 0; c < 2; c++) { __m256i b = _mm256_loadu_si256((const __m256i*)(ib + g * 64 + c * 32)); __m256i lo = _mm256_shuffle_epi8(T, _mm256_and_si256(b, m15)); __m256i hi = _mm256_shuffle_epi8(T, _mm256_and_si256(_mm256_srli_epi16(b, 4), m15)); a = dot_i8(a, _mm256_loadu_si256((const __m256i*)(ag + c * 64)), _mm256_sign_epi8(lo, _mm256_loadu_si256((const __m256i*)(xg + c * 64)))); a = dot_i8(a, _mm256_loadu_si256((const __m256i*)(ag + c * 64 + 32)), _mm256_sign_epi8(hi, _mm256_loadu_si256((const __m256i*)(xg + c * 64 + 32)))); } } acc += (float)hsum_i32(a) * qx.sx[g] * cq_scale(q, (size_t)r * groups + g); } #elif defined(OASIS_NEON) // The same integer sums as the portable path below, sixteen weights per table lookup. const int8x16_t T = vld1q_s8(t.tbl); for (int g = 0; g < groups; g++) { const int8_t* xg = qx.xs.data() + g * 128; int32x4_t a = vdupq_n_s32(0); if (q.bits == 2) { const uint8x16_t m3 = vdupq_n_u8(3); const uint8x16_t b0 = vld1q_u8(ib + g * 32), b1 = vld1q_u8(ib + g * 32 + 16); a = dot_i8(a, vqtbl1q_s8(T, vandq_u8(b0, m3)), vld1q_s8(xg)); a = dot_i8(a, vqtbl1q_s8(T, vandq_u8(b1, m3)), vld1q_s8(xg + 16)); a = dot_i8(a, vqtbl1q_s8(T, vandq_u8(vshrq_n_u8(b0, 2), m3)), vld1q_s8(xg + 32)); a = dot_i8(a, vqtbl1q_s8(T, vandq_u8(vshrq_n_u8(b1, 2), m3)), vld1q_s8(xg + 48)); a = dot_i8(a, vqtbl1q_s8(T, vandq_u8(vshrq_n_u8(b0, 4), m3)), vld1q_s8(xg + 64)); a = dot_i8(a, vqtbl1q_s8(T, vandq_u8(vshrq_n_u8(b1, 4), m3)), vld1q_s8(xg + 80)); a = dot_i8(a, vqtbl1q_s8(T, vshrq_n_u8(b0, 6)), vld1q_s8(xg + 96)); a = dot_i8(a, vqtbl1q_s8(T, vshrq_n_u8(b1, 6)), vld1q_s8(xg + 112)); } else { const uint8x16_t m15 = vdupq_n_u8(15); for (int c = 0; c < 2; c++) { const uint8_t* bp = ib + g * 64 + c * 32; const int8_t* xp = xg + c * 64; const uint8x16_t b0 = vld1q_u8(bp), b1 = vld1q_u8(bp + 16); a = dot_i8(a, vqtbl1q_s8(T, vandq_u8(b0, m15)), vld1q_s8(xp)); a = dot_i8(a, vqtbl1q_s8(T, vandq_u8(b1, m15)), vld1q_s8(xp + 16)); a = dot_i8(a, vqtbl1q_s8(T, vshrq_n_u8(b0, 4)), vld1q_s8(xp + 32)); a = dot_i8(a, vqtbl1q_s8(T, vshrq_n_u8(b1, 4)), vld1q_s8(xp + 48)); } } acc += (float)vaddvq_s32(a) * qx.sx[g] * cq_scale(q, (size_t)r * groups + g); } #else // Portable path: the same int8 arithmetic, one weight at a time. for (int g = 0; g < groups; g++) { const int8_t* xg = qx.xs.data() + g * 128; int a = 0; if (q.bits == 2) { for (int k = 0; k < 4; k++) for (int mm = 0; mm < 32; mm++) a += t.tbl[(ib[g * 32 + mm] >> (2 * k)) & 3] * xg[k * 32 + mm]; } else { for (int c = 0; c < 2; c++) for (int mm = 0; mm < 32; mm++) { uint8_t bb = ib[g * 64 + c * 32 + mm]; a += t.tbl[bb & 15] * xg[c * 64 + mm] + t.tbl[bb >> 4] * xg[c * 64 + 32 + mm]; } } acc += (float)a * qx.sx[g] * cq_scale(q, (size_t)r * groups + g); } #endif return acc * t.cs; } static void cq_rows_q(const CQ& q, const QX& qx, float* y, bool parallel) { CbTable t = cb_table(q); #pragma omp parallel for schedule(static) if (parallel) for (int r = 0; r < q.rows; r++) y[r] = q.zero_rows && q.zero_rows[r] ? 0.0f : cq_row(q, qx, r, t); } void cq_gemv_q(const CQ& q, const QX& qx, float* y) { cq_rows_q(q, qx, y, q.rows >= 256); } // C[M, rows] = A[M, cols] W^T with int8 activations per rotated 128-group, as in the deployed kernels. #if defined(OASIS_NEON) // Every row of W against four prepared inputs at once: a group's weights are looked up once for all four, // and each output gets the integer sums and the float steps of cq_row as GCC compiles it: with 8-bit scales // the group's scale times the input's, then a multiply-add of that and the sum; with fp16 scales the sum // times the input's scale, then a multiply-add of that and the group's scale. static void cq_rows4(const CQ& q, const QX* qx, const CbTable& t, float* const* y) { const int groups = q.pc / 128; const size_t row_bytes = (size_t)q.pc * q.bits / 8; const int8x16_t T = vld1q_s8(t.tbl); const uint8x16_t m3 = vdupq_n_u8(3), m15 = vdupq_n_u8(15); for (int r = 0; r < q.rows; r++) { if (q.zero_rows && q.zero_rows[r]) { for (int i = 0; i < 4; i++) y[i][r] = 0.0f; continue; } const uint8_t* ib = q.idx + (size_t)r * row_bytes; float32x4_t acc = vdupq_n_f32(0.0f); for (int g = 0; g < groups; g++) { int8x16_t w[8]; if (q.bits == 2) { const uint8x16_t b0 = vld1q_u8(ib + g * 32), b1 = vld1q_u8(ib + g * 32 + 16); w[0] = vqtbl1q_s8(T, vandq_u8(b0, m3)); w[1] = vqtbl1q_s8(T, vandq_u8(b1, m3)); w[2] = vqtbl1q_s8(T, vandq_u8(vshrq_n_u8(b0, 2), m3)); w[3] = vqtbl1q_s8(T, vandq_u8(vshrq_n_u8(b1, 2), m3)); w[4] = vqtbl1q_s8(T, vandq_u8(vshrq_n_u8(b0, 4), m3)); w[5] = vqtbl1q_s8(T, vandq_u8(vshrq_n_u8(b1, 4), m3)); w[6] = vqtbl1q_s8(T, vshrq_n_u8(b0, 6)); w[7] = vqtbl1q_s8(T, vshrq_n_u8(b1, 6)); } else { for (int c = 0; c < 2; c++) { const uint8x16_t b0 = vld1q_u8(ib + g * 64 + c * 32), b1 = vld1q_u8(ib + g * 64 + c * 32 + 16); w[4 * c] = vqtbl1q_s8(T, vandq_u8(b0, m15)); w[4 * c + 1] = vqtbl1q_s8(T, vandq_u8(b1, m15)); w[4 * c + 2] = vqtbl1q_s8(T, vshrq_n_u8(b0, 4)); w[4 * c + 3] = vqtbl1q_s8(T, vshrq_n_u8(b1, 4)); } } int32x4_t a[4]; float sx[4]; for (int i = 0; i < 4; i++) { const int8_t* xg = qx[i].xs.data() + g * 128; int32x4_t s = vdupq_n_s32(0); for (int k = 0; k < 8; k++) s = dot_i8(s, w[k], vld1q_s8(xg + 16 * k)); a[i] = s; sx[i] = qx[i].sx[g]; } const float32x4_t sum = vcvtq_f32_s32(vpaddq_s32(vpaddq_s32(a[0], a[1]), vpaddq_s32(a[2], a[3]))); const float scale = cq_scale(q, (size_t)r * groups + g); if (q.ncode) acc = vfmaq_f32(acc, vmulq_n_f32(vld1q_f32(sx), scale), sum); else acc = vfmaq_f32(acc, vmulq_f32(sum, vld1q_f32(sx)), vdupq_n_f32(scale)); } float out[4]; vst1q_f32(out, vmulq_n_f32(acc, t.cs)); for (int i = 0; i < 4; i++) y[i][r] = out[i]; } } #endif void cq_gemm_q(const CQ& q, const float* A, int M, float* C) { CbTable t = cb_table(q); #if defined(OASIS_NEON) const int blocks = (M + 3) / 4; #pragma omp parallel for schedule(static) if (M > 1) for (int b = 0; b < blocks; b++) { thread_local QX qx[4]; const int i0 = b * 4, n = std::min(4, M - i0); for (int i = 0; i < n; i++) prep_x(A + (size_t)(i0 + i) * q.cols, q.cols, q.bits, qx[i]); if (n == 4) { float* y[4] = {C + (size_t)i0 * q.rows, C + (size_t)(i0 + 1) * q.rows, C + (size_t)(i0 + 2) * q.rows, C + (size_t)(i0 + 3) * q.rows}; cq_rows4(q, qx, t, y); continue; } for (int i = 0; i < n; i++) { float* y = C + (size_t)(i0 + i) * q.rows; for (int r = 0; r < q.rows; r++) y[r] = q.zero_rows && q.zero_rows[r] ? 0.0f : cq_row(q, qx[i], r, t); } } #else #pragma omp parallel for schedule(static) if (M > 1) for (int i = 0; i < M; i++) { thread_local QX qx; prep_x(A + (size_t)i * q.cols, q.cols, q.bits, qx); float* y = C + (size_t)i * q.rows; for (int r = 0; r < q.rows; r++) y[r] = q.zero_rows && q.zero_rows[r] ? 0.0f : cq_row(q, qx, r, t); } #endif } float dotf(const float* a, const float* b, int n) { #if defined(OASIS_FMA) __m256 s0 = _mm256_setzero_ps(), s1 = _mm256_setzero_ps(); int i = 0; for (; i + 16 <= n; i += 16) { s0 = _mm256_fmadd_ps(_mm256_loadu_ps(a + i), _mm256_loadu_ps(b + i), s0); s1 = _mm256_fmadd_ps(_mm256_loadu_ps(a + i + 8), _mm256_loadu_ps(b + i + 8), s1); } s0 = _mm256_add_ps(s0, s1); __m128 h = _mm_add_ps(_mm256_castps256_ps128(s0), _mm256_extractf128_ps(s0, 1)); h = _mm_add_ps(h, _mm_movehl_ps(h, h)); h = _mm_add_ss(h, _mm_shuffle_ps(h, h, 1)); float s = _mm_cvtss_f32(h); for (; i < n; i++) s += a[i] * b[i]; return s; #elif defined(OASIS_NEON) float32x4_t s0 = vdupq_n_f32(0.0f), s1 = s0, s2 = s0, s3 = s0; int i = 0; for (; i + 16 <= n; i += 16) { s0 = vfmaq_f32(s0, vld1q_f32(a + i), vld1q_f32(b + i)); s1 = vfmaq_f32(s1, vld1q_f32(a + i + 4), vld1q_f32(b + i + 4)); s2 = vfmaq_f32(s2, vld1q_f32(a + i + 8), vld1q_f32(b + i + 8)); s3 = vfmaq_f32(s3, vld1q_f32(a + i + 12), vld1q_f32(b + i + 12)); } float s = vaddvq_f32(vaddq_f32(vaddq_f32(s0, s1), vaddq_f32(s2, s3))); for (; i < n; i++) s += a[i] * b[i]; return s; #else float s = 0.0f; for (int i = 0; i < n; i++) s += a[i] * b[i]; return s; #endif } void cq_gemv(const CQ& q, const float* x, float* y) { #if defined(__AVX2__) || defined(OASIS_NEON) if (!g_exact_gemv && (q.bits == 2 || q.bits == 4)) { thread_local QX qx; prep_x(x, q.cols, q.bits, qx); cq_gemv_q(q, qx, y); return; } #endif int groups = q.pc / 128; std::vector xt(q.pc, 0.0f); std::memcpy(xt.data(), x, sizeof(float) * q.cols); for (int gi = 0; gi < groups; gi++) wht_inplace(xt.data() + gi * 128, 128); if (q.bits == 2) { alignas(16) float tab[256][4]; for (int b = 0; b < 256; b++) for (int k = 0; k < 4; k++) tab[b][k] = q.cb[(b >> (2 * k)) & 3]; size_t row_bytes = (size_t)q.pc / 4; #pragma omp parallel for schedule(static) if (q.rows >= 256) for (int r = 0; r < q.rows; r++) { const uint8_t* ib = q.idx + (size_t)r * row_bytes; float acc = 0.0f; for (int gi = 0; gi < groups; gi++) { const uint8_t* gb = ib + gi * 32; const float* xg = xt.data() + gi * 128; float a0 = 0, a1 = 0, a2 = 0, a3 = 0; for (int m = 0; m < 32; m++) { const float* t = tab[gb[m]]; const float* xv = xg + 4 * m; a0 += t[0] * xv[0]; a1 += t[1] * xv[1]; a2 += t[2] * xv[2]; a3 += t[3] * xv[3]; } acc += (a0 + a1 + a2 + a3) * cq_scale(q, (size_t)r * groups + gi); } y[r] = acc; } return; } if (q.bits == 4) { alignas(8) float tab[256][2]; for (int b = 0; b < 256; b++) { tab[b][0] = q.cb[b & 15]; tab[b][1] = q.cb[b >> 4]; } size_t row_bytes = (size_t)q.pc / 2; #pragma omp parallel for schedule(static) if (q.rows >= 256) for (int r = 0; r < q.rows; r++) { const uint8_t* ib = q.idx + (size_t)r * row_bytes; float acc = 0.0f; for (int gi = 0; gi < groups; gi++) { const uint8_t* gb = ib + gi * 64; const float* xg = xt.data() + gi * 128; float a0 = 0, a1 = 0; for (int m = 0; m < 64; m++) { a0 += tab[gb[m]][0] * xg[2 * m]; a1 += tab[gb[m]][1] * xg[2 * m + 1]; } acc += (a0 + a1) * cq_scale(q, (size_t)r * groups + gi); } y[r] = acc; } return; } #pragma omp parallel for schedule(static) if (q.rows >= 256) for (int r = 0; r < q.rows; r++) { float acc = 0.0f; for (int gi = 0; gi < groups; gi++) { float a = 0.0f; size_t base = (size_t)r * q.pc + (size_t)gi * 128; for (int i = 0; i < 128; i++) a += q.cb[cq_index(q.idx, base + i, q.bits)] * xt[gi * 128 + i]; acc += a * cq_scale(q, (size_t)r * groups + gi); } y[r] = acc; } } void gemm_nt(const float* A, int M, int K, const float* W, int N, float* C) { if (M == 1) { #pragma omp parallel for schedule(static) if ((long)N * K > 200000) for (int j = 0; j < N; j++) C[j] = dotf(A, W + (size_t)j * K, K); return; } #pragma omp parallel for schedule(static) if ((long)M * N * K > 200000) for (int i = 0; i < M; i++) for (int j = 0; j < N; j++) C[(size_t)i * N + j] = dotf(A + (size_t)i * K, W + (size_t)j * K, K); } // C = A B. Every output accumulates its K products in order (fused multiply-adds where the target // has them), so the register-blocked path and the plain loop give the same values. static void gemm_nn_rows(const float* A, int i0, int i1, int K, const float* B, int N, float* C, int j0) { for (int i = i0; i < i1; i++) { float* c = C + (size_t)i * N; const float* a = A + (size_t)i * K; for (int j = j0; j < N; j++) c[j] = 0.0f; for (int k = 0; k < K; k++) { float av = a[k]; const float* b = B + (size_t)k * N; for (int j = j0; j < N; j++) c[j] += av * b[j]; } } } void gemm_nn(const float* A, int M, int K, const float* B, int N, float* C) { #if defined(OASIS_FMA) const int NB = N / 16 * 16; const int blocks = (M + 3) / 4; #pragma omp parallel for schedule(static) if ((long)M * N * K > 200000) for (int ib = 0; ib < blocks; ib++) { int i0 = ib * 4, mr = std::min(4, M - i0); if (mr == 4) { const float* a0 = A + (size_t)i0 * K; const float* a1 = a0 + K; const float* a2 = a1 + K; const float* a3 = a2 + K; for (int j0 = 0; j0 < NB; j0 += 16) { __m256 c00 = _mm256_setzero_ps(), c01 = _mm256_setzero_ps(), c10 = _mm256_setzero_ps(), c11 = _mm256_setzero_ps(); __m256 c20 = _mm256_setzero_ps(), c21 = _mm256_setzero_ps(), c30 = _mm256_setzero_ps(), c31 = _mm256_setzero_ps(); for (int k = 0; k < K; k++) { const float* b = B + (size_t)k * N + j0; __m256 b0 = _mm256_loadu_ps(b), b1 = _mm256_loadu_ps(b + 8), x; x = _mm256_broadcast_ss(a0 + k); c00 = _mm256_fmadd_ps(x, b0, c00); c01 = _mm256_fmadd_ps(x, b1, c01); x = _mm256_broadcast_ss(a1 + k); c10 = _mm256_fmadd_ps(x, b0, c10); c11 = _mm256_fmadd_ps(x, b1, c11); x = _mm256_broadcast_ss(a2 + k); c20 = _mm256_fmadd_ps(x, b0, c20); c21 = _mm256_fmadd_ps(x, b1, c21); x = _mm256_broadcast_ss(a3 + k); c30 = _mm256_fmadd_ps(x, b0, c30); c31 = _mm256_fmadd_ps(x, b1, c31); } float* c = C + (size_t)i0 * N + j0; _mm256_storeu_ps(c, c00); _mm256_storeu_ps(c + 8, c01); _mm256_storeu_ps(c + N, c10); _mm256_storeu_ps(c + N + 8, c11); _mm256_storeu_ps(c + 2 * N, c20); _mm256_storeu_ps(c + 2 * N + 8, c21); _mm256_storeu_ps(c + 3 * N, c30); _mm256_storeu_ps(c + 3 * N + 8, c31); } } else { for (int i = i0; i < i0 + mr; i++) { const float* a = A + (size_t)i * K; for (int j0 = 0; j0 < NB; j0 += 16) { __m256 c0 = _mm256_setzero_ps(), c1 = _mm256_setzero_ps(); for (int k = 0; k < K; k++) { const float* b = B + (size_t)k * N + j0; __m256 x = _mm256_broadcast_ss(a + k); c0 = _mm256_fmadd_ps(x, _mm256_loadu_ps(b), c0); c1 = _mm256_fmadd_ps(x, _mm256_loadu_ps(b + 8), c1); } _mm256_storeu_ps(C + (size_t)i * N + j0, c0); _mm256_storeu_ps(C + (size_t)i * N + j0 + 8, c1); } } } if (NB < N) gemm_nn_rows(A, i0, i0 + mr, K, B, N, C, NB); } #elif defined(OASIS_NEON) const int NB = N / 16 * 16; const int blocks = (M + 3) / 4; #pragma omp parallel for schedule(static) if ((long)M * N * K > 200000) for (int ib = 0; ib < blocks; ib++) { int i0 = ib * 4, mr = std::min(4, M - i0); if (mr == 4) { const float* a0 = A + (size_t)i0 * K; const float* a1 = a0 + K; const float* a2 = a1 + K; const float* a3 = a2 + K; for (int j0 = 0; j0 < NB; j0 += 16) { float32x4_t c00 = vdupq_n_f32(0.0f), c01 = c00, c02 = c00, c03 = c00, c10 = c00, c11 = c00, c12 = c00, c13 = c00; float32x4_t c20 = c00, c21 = c00, c22 = c00, c23 = c00, c30 = c00, c31 = c00, c32 = c00, c33 = c00; for (int k = 0; k < K; k++) { const float* b = B + (size_t)k * N + j0; const float32x4_t b0 = vld1q_f32(b), b1 = vld1q_f32(b + 4), b2 = vld1q_f32(b + 8), b3 = vld1q_f32(b + 12); float x = a0[k]; c00 = vfmaq_n_f32(c00, b0, x); c01 = vfmaq_n_f32(c01, b1, x); c02 = vfmaq_n_f32(c02, b2, x); c03 = vfmaq_n_f32(c03, b3, x); x = a1[k]; c10 = vfmaq_n_f32(c10, b0, x); c11 = vfmaq_n_f32(c11, b1, x); c12 = vfmaq_n_f32(c12, b2, x); c13 = vfmaq_n_f32(c13, b3, x); x = a2[k]; c20 = vfmaq_n_f32(c20, b0, x); c21 = vfmaq_n_f32(c21, b1, x); c22 = vfmaq_n_f32(c22, b2, x); c23 = vfmaq_n_f32(c23, b3, x); x = a3[k]; c30 = vfmaq_n_f32(c30, b0, x); c31 = vfmaq_n_f32(c31, b1, x); c32 = vfmaq_n_f32(c32, b2, x); c33 = vfmaq_n_f32(c33, b3, x); } float* c = C + (size_t)i0 * N + j0; vst1q_f32(c, c00); vst1q_f32(c + 4, c01); vst1q_f32(c + 8, c02); vst1q_f32(c + 12, c03); c += N; vst1q_f32(c, c10); vst1q_f32(c + 4, c11); vst1q_f32(c + 8, c12); vst1q_f32(c + 12, c13); c += N; vst1q_f32(c, c20); vst1q_f32(c + 4, c21); vst1q_f32(c + 8, c22); vst1q_f32(c + 12, c23); c += N; vst1q_f32(c, c30); vst1q_f32(c + 4, c31); vst1q_f32(c + 8, c32); vst1q_f32(c + 12, c33); } } else { for (int i = i0; i < i0 + mr; i++) { const float* a = A + (size_t)i * K; for (int j0 = 0; j0 < NB; j0 += 16) { float32x4_t c0 = vdupq_n_f32(0.0f), c1 = c0, c2 = c0, c3 = c0; for (int k = 0; k < K; k++) { const float* b = B + (size_t)k * N + j0; const float x = a[k]; c0 = vfmaq_n_f32(c0, vld1q_f32(b), x); c1 = vfmaq_n_f32(c1, vld1q_f32(b + 4), x); c2 = vfmaq_n_f32(c2, vld1q_f32(b + 8), x); c3 = vfmaq_n_f32(c3, vld1q_f32(b + 12), x); } float* c = C + (size_t)i * N + j0; vst1q_f32(c, c0); vst1q_f32(c + 4, c1); vst1q_f32(c + 8, c2); vst1q_f32(c + 12, c3); } } } if (NB < N) gemm_nn_rows(A, i0, i0 + mr, K, B, N, C, NB); } #else #pragma omp parallel for schedule(static) if ((long)M * N * K > 200000) for (int i = 0; i < M; i++) gemm_nn_rows(A, i, i + 1, K, B, N, C, 0); #endif } void zc_norm(const float* x, int n, const float* scale, float* out) { double ss = 0.0; for (int i = 0; i < n; i++) ss += (double)x[i] * x[i]; float r = 1.0f / std::sqrt((float)(ss / n) + 1e-6f); for (int i = 0; i < n; i++) out[i] = x[i] * r * (1.0f + scale[i]); } void rms_unit(const float* x, int n, float* out) { double ss = 0.0; for (int i = 0; i < n; i++) ss += (double)x[i] * x[i]; float r = 1.0f / std::sqrt((float)(ss / n) + 1e-6f); for (int i = 0; i < n; i++) out[i] = x[i] * r; } void band65(const float* u, const float* w, int n, float* out) { for (int j = 0; j < n; j++) { const float* wj = w + (size_t)j * 65; int lo = std::max(0, 32 - j), hi = std::min(65, n + 32 - j); float s = 0.0f; for (int t = lo; t < hi; t++) s += wj[t] * u[j + t - 32]; out[j] = s; } } void qmm(Model& m, const std::string& name, const float* A, int M, float* C) { CQ q = m.Q(name); if (g_exact_gemv) gemm_nt(A, M, q.cols, m.Wdq(name), q.rows, C); else cq_gemm_q(q, A, M, C); } } // namespace oasis // ==== Image side: Pillow-exact bilinear resize, encoder, neck, detection, masks, box slots and memory namespace oasis { // ------------------------------------------------------------------ resize (matches Pillow 8bpc) // The resize follows the fixed-point bilinear filter of Pillow, so that the engine reads the same // pixels as the reference implementation. Pillow carries this notice: // // The Python Imaging Library (PIL) is // // Copyright © 1997-2011 by Secret Labs AB // Copyright © 1995-2011 by Fredrik Lundh and contributors // // Pillow is the friendly PIL fork. It is // // Copyright © 2010 by Jeffrey A. Clark and contributors // // Like PIL, Pillow is licensed under the open source MIT-CMU License: // // By obtaining, using, and/or copying this software and/or its associated // documentation, you agree that you have read, understood, and will comply // with the following terms and conditions: // // Permission to use, copy, modify and distribute this software and its // documentation for any purpose and without fee is hereby granted, // provided that the above copyright notice appears in all copies, and that // both that copyright notice and this permission notice appear in supporting // documentation, and that the name of Secret Labs AB or the author not be // used in advertising or publicity pertaining to distribution of the software // without specific, written prior permission. // // SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS // SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. // IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, // INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. static const int PREC = 22; static void coeffs(int in_size, int out_size, std::vector& bounds, std::vector& kk, int& ksize) { double scale = (double)in_size / out_size, fs = scale < 1.0 ? 1.0 : scale; double support = 1.0 * fs; ksize = (int)std::ceil(support) * 2 + 1; bounds.assign(out_size * 2, 0); kk.assign((size_t)out_size * ksize, 0); std::vector k(ksize); for (int xx = 0; xx < out_size; xx++) { double center = (xx + 0.5) * scale, ww = 0.0, ss = 1.0 / fs; int xmin = (int)(center - support + 0.5); if (xmin < 0) xmin = 0; int xmax = (int)(center + support + 0.5); if (xmax > in_size) xmax = in_size; xmax -= xmin; for (int x = 0; x < xmax; x++) { double t = (x + xmin - center + 0.5) * ss; double w = t < 0 ? -t : t; w = w < 1.0 ? 1.0 - w : 0.0; k[x] = w; ww += w; } for (int x = 0; x < xmax; x++) { double v = ww != 0.0 ? k[x] / ww : 0.0; kk[(size_t)xx * ksize + x] = v < 0 ? (int)(-0.5 + v * (1 << PREC)) : (int)(0.5 + v * (1 << PREC)); } bounds[xx * 2] = xmin; bounds[xx * 2 + 1] = xmax; } } static inline uint8_t clip8(long long v) { if (v >= ((long long)256 << PREC)) return 255; if (v <= 0) return 0; return (uint8_t)(v >> PREC); } std::vector resize_rgb(const uint8_t* src, int W0, int H0, int W1, int H1) { std::vector tmp((size_t)H0 * W1 * 3), out((size_t)H1 * W1 * 3); std::vector b, k; int ks; if (W1 != W0) { coeffs(W0, W1, b, k, ks); #pragma omp parallel for schedule(static) if ((long)H0 * W1 > 65536) for (int y = 0; y < H0; y++) for (int x = 0; x < W1; x++) for (int c = 0; c < 3; c++) { long long ss = 1LL << (PREC - 1); for (int t = 0; t < b[x * 2 + 1]; t++) ss += (long long)src[((size_t)y * W0 + b[x * 2] + t) * 3 + c] * k[(size_t)x * ks + t]; tmp[((size_t)y * W1 + x) * 3 + c] = clip8(ss); } } else { std::memcpy(tmp.data(), src, tmp.size()); } if (H1 != H0) { coeffs(H0, H1, b, k, ks); #pragma omp parallel for schedule(static) if ((long)H1 * W1 > 65536) for (int y = 0; y < H1; y++) for (int x = 0; x < W1; x++) for (int c = 0; c < 3; c++) { long long ss = 1LL << (PREC - 1); for (int t = 0; t < b[y * 2 + 1]; t++) ss += (long long)tmp[((size_t)(b[y * 2] + t) * W1 + x) * 3 + c] * k[(size_t)y * ks + t]; out[((size_t)y * W1 + x) * 3 + c] = clip8(ss); } } else { out = tmp; } return out; } // ------------------------------------------------------------------ NHWC building blocks static std::vector valid_mask(int n, int vh, int vw) { std::vector m((size_t)n * n, 0); for (int i = 0; i < std::min(n, vh); i++) for (int j = 0; j < std::min(n, vw); j++) m[(size_t)i * n + j] = 1; return m; } // 3x3 convolution with one pixel of zero padding on every side, HWIO weights. static void conv3x3(const float* in, int H, int W, int Ci, const float* w, int Co, int sh, int sw, std::vector& out, int& Ho, int& Wo) { Ho = (H - 1) / sh + 1; Wo = (W - 1) / sw + 1; out.assign((size_t)Ho * Wo * Co, 0.0f); #pragma omp parallel for schedule(static) for (int y = 0; y < Ho; y++) for (int x = 0; x < Wo; x++) { float* o = out.data() + ((size_t)y * Wo + x) * Co; for (int kh = 0; kh < 3; kh++) { int iy = y * sh - 1 + kh; if (iy < 0 || iy >= H) continue; for (int kw = 0; kw < 3; kw++) { int ix = x * sw - 1 + kw; if (ix < 0 || ix >= W) continue; const float* ip = in + ((size_t)iy * W + ix) * Ci; const float* wp = w + ((size_t)(kh * 3 + kw) * Ci) * Co; for (int ci = 0; ci < Ci; ci++) { float v = ip[ci]; const float* wr = wp + (size_t)ci * Co; for (int co = 0; co < Co; co++) o[co] += v * wr[co]; } } } } } // Depthwise kh x kw convolution, weights [kh, kw, 1, C]; padding kh/2, kw/2. static void dwconv(const float* in, int H, int W, int C, const float* w, int kh_, int kw_, int sh, int sw, std::vector& out, int& Ho, int& Wo) { int ph = kh_ / 2, pw_ = kw_ / 2; Ho = (H + 2 * ph - kh_) / sh + 1; Wo = (W + 2 * pw_ - kw_) / sw + 1; out.assign((size_t)Ho * Wo * C, 0.0f); #pragma omp parallel for schedule(static) for (int y = 0; y < Ho; y++) for (int x = 0; x < Wo; x++) { float* o = out.data() + ((size_t)y * Wo + x) * C; for (int a = 0; a < kh_; a++) { int iy = y * sh - ph + a; if (iy < 0 || iy >= H) continue; for (int bb = 0; bb < kw_; bb++) { int ix = x * sw - pw_ + bb; if (ix < 0 || ix >= W) continue; const float* ip = in + ((size_t)iy * W + ix) * C; const float* wp = w + (size_t)(a * kw_ + bb) * C; for (int c = 0; c < C; c++) o[c] += ip[c] * wp[c]; } } } } // Fixed 4096-element blocks, so every element is computed the same way whatever the thread count. static void silu_v(std::vector& v) { const int n = (int)v.size(), B = 4096, nb = (n + B - 1) / B; float* p = v.data(); #pragma omp parallel for schedule(static) if (n > 65536) for (int b = 0; b < nb; b++) { int e = std::min(n, (b + 1) * B); #if defined(OASIS_NEON) const int i0 = b * B, n4 = (e - i0) & ~3; silu_inplace(p + i0, n4); for (int i = i0 + n4; i < e; i++) p[i] = siluf(p[i]); #else for (int i = b * B; i < e; i++) p[i] = siluf(p[i]); #endif } } static void norm_rows(std::vector& x, int C, const float* scale) { size_t n = x.size() / C; #pragma omp parallel for schedule(static) if (n > 64) for (long p = 0; p < (long)n; p++) zc_norm(x.data() + p * C, C, scale, x.data() + p * C); } static void apply_mask(std::vector& x, int C, const std::vector& m) { for (size_t p = 0; p < m.size(); p++) if (!m[p]) std::memset(x.data() + p * C, 0, sizeof(float) * C); } static std::vector pointwise(const std::vector& x, int Ci, const float* k, int Co) { int P = (int)(x.size() / Ci); std::vector out((size_t)P * Co); gemm_nn(x.data(), P, Ci, k, Co, out.data()); return out; } static std::vector up2(const std::vector& x, int n, int C) { std::vector o((size_t)4 * n * n * C); for (int i = 0; i < 2 * n; i++) for (int j = 0; j < 2 * n; j++) std::memcpy(o.data() + ((size_t)i * 2 * n + j) * C, x.data() + ((size_t)(i / 2) * n + j / 2) * C, sizeof(float) * C); return o; } // One HConvGLU's weights, looked up once per block rather than once per cell. struct HConv { const float *gate, *gate_bias, *value, *out; }; static HConv hconv_of(Model& m, const std::string& pre) { return {m.F(pre + "/gate_conv"), m.F(pre + "/gate_bias"), m.F(pre + "/value_conv"), m.F(pre + "/out_conv")}; } #if defined(OASIS_NEON) // The bands of NC cells at once, with tap-major weights w [65, 512] (Model::load stores them so on Arm): // every tap is one multiply-add across the outputs of all NC cells, so each weight vector is loaded once // for all of them. up[c] holds a cell's input with 32 zeros on either side. template static void band_cells(float (*up)[576], const float* w, float (*out)[512]) { for (int j = 0; j < 512; j += 8) { float32x4_t a[NC][2]; for (int c = 0; c < NC; c++) a[c][0] = a[c][1] = vdupq_n_f32(0.0f); for (int t = 0; t < 65; t++) { const float32x4_t w0 = vld1q_f32(w + (size_t)t * 512 + j), w1 = vld1q_f32(w + (size_t)t * 512 + j + 4); for (int c = 0; c < NC; c++) { a[c][0] = vfmaq_f32(a[c][0], w0, vld1q_f32(up[c] + j + t)); a[c][1] = vfmaq_f32(a[c][1], w1, vld1q_f32(up[c] + j + t + 4)); } } for (int c = 0; c < NC; c++) { vst1q_f32(out[c] + j, a[c][0]); vst1q_f32(out[c] + j + 4, a[c][1]); } } } static void band_cells(int nc, float (*up)[576], const float* w, float (*out)[512]) { switch (nc) { case 4: band_cells<4>(up, w, out); break; case 3: band_cells<3>(up, w, out); break; case 2: band_cells<2>(up, w, out); break; default: band_cells<1>(up, w, out); break; } } // HConvGLU of up to four cells at once: x[c] -> y[c]. static void hconvglu_cells(const Model& m, const HConv& w, const float* const* x, float* const* y, int nc) { float up[4][576], a[4][512], v[4][512], z[4][512]; for (int c = 0; c < nc; c++) { std::memset(up[c], 0, sizeof up[c]); wht_seq512(m, x[c], up[c] + 32); } band_cells(nc, up, w.gate, a); band_cells(nc, up, w.value, v); // gelu_tanh(a + bias) * v with the operations the scalar loop compiles to, k * fma(0.044715 x, x^2, x) and // ((1 + tanh) v)(0.5 x), and tanh four at a time. for (int c = 0; c < nc; c++) for (int i = 0; i < 512; i += 4) { const float32x4_t x = vaddq_f32(vld1q_f32(a[c] + i), vld1q_f32(w.gate_bias + i)); const float32x4_t arg = vmulq_n_f32(vfmaq_f32(x, vmulq_n_f32(x, 0.044715f), vmulq_f32(x, x)), 0.7978845608028654f); const float32x4_t t = vaddq_f32(tanhf4(arg), vdupq_n_f32(1.0f)); vst1q_f32(up[c] + 32 + i, vmulq_f32(vmulq_f32(t, vld1q_f32(v[c] + i)), vmulq_n_f32(x, 0.5f))); } band_cells(nc, up, w.out, z); for (int c = 0; c < nc; c++) wht_seq512(m, z[c], y[c]); } #else static void hconvglu(const Model& m, const HConv& w, const float* x, float* y) { float u[512], a[512], v[512], h[512], z[512]; wht_seq512(m, x, u); band65(u, w.gate, 512, a); band65(u, w.value, 512, v); for (int i = 0; i < 512; i++) h[i] = gelu_tanh(a[i] + w.gate_bias[i]) * v[i]; band65(h, w.out, 512, z); wht_seq512(m, z, y); } #endif // residual block B(F) = F + PW(DW3x3(SiLU(N(F)))), masked static void res_block(Model& m, std::vector& F, int n, int C, const std::string& norm, const std::string& dw, const std::string& pw, const std::vector& mask) { std::vector t = F, d; norm_rows(t, C, m.F(norm)); silu_v(t); int ho, wo; dwconv(t.data(), n, n, C, m.F(dw), 3, 3, 1, 1, d, ho, wo); std::vector p = pointwise(d, C, m.F(pw), C); for (size_t i = 0; i < F.size(); i++) F[i] += p[i]; apply_mask(F, C, mask); } // Stage checksums on stderr when OASIS_DEBUG=1, for comparing builds. static void stage(const char* name, const std::vector& v) { static const bool on = [] { const char* d = std::getenv("OASIS_DEBUG"); return d && d[0] == '1'; }(); if (!on) return; double s = 0.0, a = 0.0; for (float x : v) { s += x; a += std::fabs(x); } std::fprintf(stderr, "%-10s n=%zu sum=%.6g abs=%.6g first=%.6g\n", name, v.size(), s, a, v.empty() ? 0.0 : v[0]); } Encoded encode(Model& m, const uint8_t* rgb, int W0, int H0) { Encoded e; float r = 512.0f / (float)std::max(W0, H0); int w = std::max(1, (int)std::nearbyint((double)W0 * r)), h = std::max(1, (int)std::nearbyint((double)H0 * r)); e.meta = {W0, H0, w, h, r}; std::vector rs = (w == W0 && h == H0) ? std::vector(rgb, rgb + (size_t)W0 * H0 * 3) : resize_rgb(rgb, W0, H0, w, h); std::vector canvas((size_t)512 * 512 * 3, 0.0f); #pragma omp parallel for schedule(static) for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) for (int c = 0; c < 3; c++) canvas[((size_t)y * 512 + x) * 3 + c] = rs[((size_t)y * w + x) * 3 + c] / 127.5f - 1.0f; auto vm = [&](int s) { return valid_mask(512 / s, (h + s - 1) / s, (w + s - 1) / s); }; auto m2 = vm(2), m4 = vm(4); e.valid8 = vm(8); e.valid16 = vm(16); e.valid32 = vm(32); e.valid4 = m4; stage("canvas", canvas); std::vector f, t, d; int Ho, Wo; conv3x3(canvas.data(), 512, 512, 3, m.F("stem/w0"), 32, 2, 2, f, Ho, Wo); norm_rows(f, 32, m.F("stem/norm_0/scale")); apply_mask(f, 32, m2); int C = 32, n = 256; const int chans[5] = {32, 64, 128, 256, 512}; std::vector feats[6]; for (int j = 1; j <= 4; j++) { t = f; silu_v(t); dwconv(t.data(), n, n, C, m.F("stem/dw_" + std::to_string(j)), 3, 3, 2, 2, d, Ho, Wo); f = pointwise(d, C, m.F("stem/pw_" + std::to_string(j) + "/kernel"), chans[j]); C = chans[j]; n = Ho; norm_rows(f, C, m.F("stem/norm_" + std::to_string(j) + "/scale")); apply_mask(f, C, j == 1 ? m4 : j == 2 ? e.valid8 : j == 3 ? e.valid16 : e.valid32); feats[j] = f; } for (int j = 1; j <= 4; j++) stage(("stem" + std::to_string(j)).c_str(), feats[j]); e.S4 = feats[1]; silu_v(e.S4); std::vector S8 = feats[2], S16 = feats[3], X = feats[4]; silu_v(S8); silu_v(S16); silu_v(X); // ---- convolutional body, 16 x 16 x 512 const auto& v32 = e.valid32; for (int i = 0; i < 16; i++) { std::string pre = "body_" + std::to_string(i); std::vector Z = X, va, ga, mix; norm_rows(Z, 512, m.F(pre + "/mix_norm/scale")); dwconv(Z.data(), 16, 16, 512, m.F(pre + "/mix/dw_value"), 3, 3, 1, 1, va, Ho, Wo); dwconv(Z.data(), 16, 16, 512, m.F(pre + "/mix/dw_gate"), 3, 3, 1, 1, ga, Ho, Wo); const float* bv = m.F(pre + "/mix/b_value"); const float* bg = m.F(pre + "/mix/b_gate"); #pragma omp parallel for schedule(static) for (int p = 0; p < 256; p++) for (int c = 0; c < 512; c++) { size_t k = (size_t)p * 512 + c; va[k] = (va[k] + bv[c]) * siluf(ga[k] + bg[c]); } dwconv(va.data(), 16, 16, 512, m.F(pre + "/mix/dw_down"), 3, 3, 1, 1, mix, Ho, Wo); for (int p = 0; p < 256; p++) if (v32[p]) for (int c = 0; c < 512; c++) X[(size_t)p * 512 + c] += mix[(size_t)p * 512 + c]; std::vector Hh((size_t)256 * 512); const float* hs = m.F(pre + "/hconv_norm/scale"); const HConv hw = hconv_of(m, pre + "/hconv"); #if defined(OASIS_NEON) #pragma omp parallel for schedule(static) for (int b = 0; b < 64; b++) { float xn[4][512]; const float* xs[4]; float* ys[4]; for (int c = 0; c < 4; c++) { const int p = b * 4 + c; zc_norm(X.data() + (size_t)p * 512, 512, hs, xn[c]); xs[c] = xn[c]; ys[c] = Hh.data() + (size_t)p * 512; } hconvglu_cells(m, hw, xs, ys, 4); } #else #pragma omp parallel for schedule(static) for (int p = 0; p < 256; p++) { float xn[512]; zc_norm(X.data() + (size_t)p * 512, 512, hs, xn); hconvglu(m, hw, xn, Hh.data() + (size_t)p * 512); } #endif const float* gam = m.F(pre + "/grn/gamma"); const float* bet = m.F(pre + "/grn/beta"); std::vector rc(512); #pragma omp parallel for schedule(static) for (int c = 0; c < 512; c++) { double s = 0.0; for (int p = 0; p < 256; p++) if (v32[p]) s += (double)Hh[(size_t)p * 512 + c] * Hh[(size_t)p * 512 + c]; rc[c] = (float)std::sqrt(s + 1e-6); } double rbar = 0.0; for (int c = 0; c < 512; c++) rbar += rc[c]; float rb = (float)(rbar / 512.0); #pragma omp parallel for schedule(static) for (int p = 0; p < 256; p++) if (v32[p]) for (int c = 0; c < 512; c++) { float hv = Hh[(size_t)p * 512 + c]; X[(size_t)p * 512 + c] += hv + gam[c] * hv * rc[c] / (rb + 1e-6f) + bet[c]; } } stage("body", X); // ---- attention blocks over 256 cells + 4 registers const int T = 260; std::vector Z((size_t)T * 512); std::memcpy(Z.data(), X.data(), sizeof(float) * 256 * 512); std::memcpy(Z.data() + 256 * 512, m.F("registers"), sizeof(float) * 4 * 512); std::vector tv(T, 1); for (int p = 0; p < 256; p++) tv[p] = v32[p]; #if defined(OASIS_NEON) std::vector active; // the valid tokens, in order, for the four-at-a-time HConvGLU for (int p = 0; p < T; p++) if (tv[p]) active.push_back(p); #endif std::vector E((size_t)256 * 512, 0.0f); auto add_level = [&](const std::vector& Zl, int l) { const float* sc = m.F("level_norm_" + std::to_string(l) + "/scale"); const float* g = m.F("level_gates") + (size_t)l * 512; #pragma omp parallel for schedule(static) for (int p = 0; p < 256; p++) { float tmp[512]; zc_norm(Zl.data() + (size_t)p * 512, 512, sc, tmp); for (int c = 0; c < 512; c++) E[(size_t)p * 512 + c] += g[c] * tmp[c]; } }; float cosr[256][16], sinr[256][16]; for (int p = 0; p < 256; p++) for (int r2 = 0; r2 < 8; r2++) { double fr = 1.0 / std::pow(10000.0, (2.0 * r2) / 16.0); double ai = (p / 16) * fr, aj = (p % 16) * fr; cosr[p][r2] = (float)std::cos(ai); sinr[p][r2] = (float)std::sin(ai); cosr[p][8 + r2] = (float)std::cos(aj); sinr[p][8 + r2] = (float)std::sin(aj); } std::vector z((size_t)T * 512), q((size_t)T * 128), k((size_t)T * 32), v((size_t)T * 64), gt((size_t)T * 256), o((size_t)T * 256), A((size_t)T * 512); for (int l = 0; l < 12; l++) { add_level(Z, l); std::string pre = "top_" + std::to_string(l) + "/"; const float* an = m.F(pre + "attn_norm/scale"); for (int p = 0; p < T; p++) zc_norm(Z.data() + (size_t)p * 512, 512, an, z.data() + (size_t)p * 512); qmm(m, pre + "self_attn/q_proj/kernel", z.data(), T, q.data()); qmm(m, pre + "self_attn/k_proj/kernel", z.data(), T, k.data()); qmm(m, pre + "self_attn/v_proj/kernel", z.data(), T, v.data()); qmm(m, pre + "self_attn/gate_proj/kernel", z.data(), T, gt.data()); const float* qn = m.F(pre + "self_attn/q_norm/scale"); const float* kn = m.F(pre + "self_attn/k_norm/scale"); auto rope = [&](float* x, int p) { if (p >= 256) return; float x1[16], x2[16]; std::memcpy(x1, x, sizeof x1); std::memcpy(x2, x + 16, sizeof x2); for (int i = 0; i < 16; i++) { x[i] = x1[i] * cosr[p][i] - x2[i] * sinr[p][i]; x[16 + i] = x2[i] * cosr[p][i] + x1[i] * sinr[p][i]; } }; for (int p = 0; p < T; p++) { for (int hh = 0; hh < 4; hh++) { float* qp = q.data() + (size_t)p * 128 + hh * 32; zc_norm(qp, 32, qn, qp); rope(qp, p); } float* kp = k.data() + (size_t)p * 32; zc_norm(kp, 32, kn, kp); rope(kp, p); } #pragma omp parallel for schedule(static) for (int p = 0; p < T; p++) { #if defined(OASIS_NEON) std::vector s(T), e(T); #else std::vector s(T); #endif for (int hh = 0; hh < 4; hh++) { const float* qp = q.data() + (size_t)p * 128 + hh * 32; float mx = -1e30f; for (int j = 0; j < T; j++) { if (!tv[j]) { s[j] = -1e30f; continue; } float d = 0.0f; for (int c = 0; c < 32; c++) d += qp[c] * k[(size_t)j * 32 + c]; s[j] = d / std::sqrt(32.0f); mx = std::max(mx, s[j]); } double den = 0.0; #if defined(OASIS_NEON) for (int j = 0; j < T; j += 4) vst1q_f32(e.data() + j, expf4(vsubq_f32(vld1q_f32(s.data() + j), vdupq_n_f32(mx)))); for (int j = 0; j < T; j++) { s[j] = tv[j] ? e[j] : 0.0f; den += s[j]; } #else for (int j = 0; j < T; j++) { s[j] = tv[j] ? std::exp(s[j] - mx) : 0.0f; den += s[j]; } #endif float* op = o.data() + (size_t)p * 256 + hh * 64; for (int c = 0; c < 64; c++) op[c] = 0.0f; for (int j = 0; j < T; j++) { if (s[j] == 0.0f) continue; float wv = (float)(s[j] / den); for (int c = 0; c < 64; c++) op[c] += wv * v[(size_t)j * 64 + c]; } #if defined(OASIS_NEON) mul_sigmoid(op, gt.data() + (size_t)p * 256 + hh * 64, 64); #else for (int c = 0; c < 64; c++) op[c] *= sigmoidf(gt[(size_t)p * 256 + hh * 64 + c]); #endif } } qmm(m, pre + "self_attn/out_proj/kernel", o.data(), T, A.data()); float ag = sigmoidf(m.F(pre + "attn_gate")[0]); const float* pn = m.F(pre + "post_attn_norm/scale"); const float* hn = m.F(pre + "hconv_norm/scale"); const HConv hw = hconv_of(m, pre + "hconv"); #if defined(OASIS_NEON) const int nb = ((int)active.size() + 3) / 4; #pragma omp parallel for schedule(static) for (int b = 0; b < nb; b++) { const int nc = std::min(4, (int)active.size() - b * 4); float tmp[4][512], hy[4][512]; const float* xs[4]; float* ys[4]; for (int c = 0; c < nc; c++) { float* zp = Z.data() + (size_t)active[b * 4 + c] * 512; float t1[512]; zc_norm(A.data() + (size_t)active[b * 4 + c] * 512, 512, pn, t1); for (int k = 0; k < 512; k++) zp[k] += ag * t1[k]; zc_norm(zp, 512, hn, tmp[c]); xs[c] = tmp[c]; ys[c] = hy[c]; } hconvglu_cells(m, hw, xs, ys, nc); for (int c = 0; c < nc; c++) { float* zp = Z.data() + (size_t)active[b * 4 + c] * 512; for (int k = 0; k < 512; k++) zp[k] += hy[c][k]; } } #else #pragma omp parallel for schedule(static) for (int p = 0; p < T; p++) { if (!tv[p]) continue; float tmp[512], hy[512]; float* zp = Z.data() + (size_t)p * 512; zc_norm(A.data() + (size_t)p * 512, 512, pn, tmp); for (int c = 0; c < 512; c++) zp[c] += ag * tmp[c]; zc_norm(zp, 512, hn, tmp); hconvglu(m, hw, tmp, hy); for (int c = 0; c < 512; c++) zp[c] += hy[c]; } #endif } { const float* en = m.F("enc_norm/scale"); float tmp[512]; for (int p = 0; p < 256; p++) { zc_norm(Z.data() + (size_t)p * 512, 512, en, tmp); for (int c = 0; c < 512; c++) E[(size_t)p * 512 + c] += tmp[c]; } apply_mask(E, 512, v32); } stage("E", E); e.E = E; // ---- neck to D [64, 64, 128] std::vector co = pointwise(E, 512, m.F("dense/coarse/kernel"), 128); std::vector tt = up2(co, 16, 128), mid = pointwise(S16, 256, m.F("dense/mid/kernel"), 128); for (size_t i = 0; i < tt.size(); i++) tt[i] += mid[i]; apply_mask(tt, 128, e.valid16); res_block(m, tt, 32, 128, "dense/neck_norm/scale", "dense/neck_dw", "dense/neck_pw/kernel", e.valid16); std::vector dd = up2(tt, 32, 128), fine = pointwise(S8, 128, m.F("dense/fine/kernel"), 128); for (size_t i = 0; i < dd.size(); i++) dd[i] += fine[i]; apply_mask(dd, 128, e.valid8); for (int i = 0; i < 3; i++) res_block(m, dd, 64, 128, "dense/block_norm_" + std::to_string(i) + "/scale", "dense/dw_" + std::to_string(i), "dense/pw_" + std::to_string(i) + "/kernel", e.valid8); norm_rows(dd, 128, m.F("dense/final_norm/scale")); silu_v(dd); apply_mask(dd, 128, e.valid8); stage("D", dd); e.D = dd; e.det = pointwise(dd, 128, m.F("dense/out/kernel"), 10); const float* db = m.F("dense/bias"); for (int p = 0; p < 64 * 64; p++) for (int c = 0; c < 10; c++) e.det[(size_t)p * 10 + c] += db[c]; stage("det", e.det); return e; } // ------------------------------------------------------------------ detection static float iou_box(const Box& a, const Box& b) { float ix = std::max(0.0f, std::min(a.x1, b.x1) - std::max(a.x0, b.x0)); float iy = std::max(0.0f, std::min(a.y1, b.y1) - std::max(a.y0, b.y0)); float inter = ix * iy; float u = (a.x1 - a.x0) * (a.y1 - a.y0) + (b.x1 - b.x0) * (b.y1 - b.y0) - inter; return u > 0 ? inter / u : 0.0f; } void detect(const Encoded& enc, float conf, float text_conf, bool nms, int max_obj, int max_txt, std::vector& objects, std::vector& texts) { for (int ch = 0; ch < 2; ch++) { float thr = ch == 0 ? conf : text_conf; int limit = ch == 0 ? max_obj : max_txt; std::vector heat(64 * 64); for (int p = 0; p < 64 * 64; p++) heat[p] = sigmoidf(enc.det[(size_t)p * 10 + ch]); struct Cand { float s; int i, j; }; std::vector c; for (int i = 0; i < 64; i++) for (int j = 0; j < 64; j++) { int p = i * 64 + j; if (!enc.valid8[p] || heat[p] < thr) continue; bool peak = true; for (int di = -1; di <= 1 && peak; di++) for (int dj = -1; dj <= 1; dj++) { int a = i + di, b = j + dj; if (a < 0 || b < 0 || a >= 64 || b >= 64) continue; if (heat[a * 64 + b] > heat[p]) { peak = false; break; } } if (peak) c.push_back({heat[p], i, j}); } std::stable_sort(c.begin(), c.end(), [](const Cand& a, const Cand& b) { return a.s > b.s; }); std::vector& out = ch == 0 ? objects : texts; float vw = enc.meta.w / 512.0f, vh = enc.meta.h / 512.0f; for (auto& cd : c) { const float* d = enc.det.data() + ((size_t)cd.i * 64 + cd.j) * 10; float w = std::fabs(d[2 + 2 * ch]), h = std::fabs(d[3 + 2 * ch]); float cx = cd.j + d[6 + 2 * ch], cy = cd.i + d[7 + 2 * ch]; Box b{(cx - w / 2) / 64, (cy - h / 2) / 64, (cx + w / 2) / 64, (cy + h / 2) / 64, cd.s, cd.i, cd.j}; b.x0 = std::clamp(b.x0, 0.0f, vw); b.x1 = std::clamp(b.x1, 0.0f, vw); b.y0 = std::clamp(b.y0, 0.0f, vh); b.y1 = std::clamp(b.y1, 0.0f, vh); bool dup = false; if (nms) for (auto& x : out) if (iou_box(b, x) > 0.5f) { dup = true; break; } if (dup) continue; out.push_back(b); if ((int)out.size() >= limit) break; } } } // ------------------------------------------------------------------ masks static std::vector head_branch(Model& m, std::vector F, int n, const std::string& pre, const std::vector& mask, const std::string& outk) { for (int i = 0; i < 2; i++) res_block(m, F, n, 128, "dense/" + pre + "_norm_" + std::to_string(i) + "/scale", "dense/" + pre + "_dw_" + std::to_string(i), "dense/" + pre + "_pw_" + std::to_string(i) + "/kernel", mask); norm_rows(F, 128, m.F("dense/" + pre + "_final_norm/scale")); silu_v(F); return pointwise(F, 128, m.F(outk), 32); } // Mask coefficients at one centre cell. The coefficient branch's only spatial operations are two 3x3 // depthwise convolutions, so its output at a cell depends on the 5 x 5 neighbourhood of D alone; cells // outside the grid enter as masked zeros, as the full branch's padding does. static std::vector coef_at(Model& m, const Encoded& enc, int ci, int cj) { std::vector F((size_t)25 * 128, 0.0f); std::vector pm(25, 0); for (int di = -2; di <= 2; di++) for (int dj = -2; dj <= 2; dj++) { int i = ci + di, j = cj + dj, p = (di + 2) * 5 + (dj + 2); if (i < 0 || j < 0 || i >= 64 || j >= 64) continue; pm[p] = enc.valid8[(size_t)i * 64 + j]; std::memcpy(F.data() + (size_t)p * 128, enc.D.data() + ((size_t)i * 64 + j) * 128, sizeof(float) * 128); } std::vector C = head_branch(m, F, 5, "coef", pm, "dense/coef/kernel"); return std::vector(C.begin() + 12 * 32, C.begin() + 13 * 32); } std::vector mask_prob(Model& m, const Encoded& enc, const Box& b, std::vector* protos) { if (protos->empty()) { std::vector U = up2(enc.D, 64, 128), ed = pointwise(enc.S4, 64, m.F("dense/edge/kernel"), 128); for (size_t i = 0; i < U.size(); i++) U[i] += ed[i]; apply_mask(U, 128, enc.valid4); *protos = head_branch(m, U, 128, "proto", enc.valid4, "dense/protos/kernel"); } const float* P = protos->data(); std::vector coef = coef_at(m, enc, b.ci, b.cj); const float* a = coef.data(); std::vector out(128 * 128, 0.0f); int ci = std::min(127, (int)((b.y0 + b.y1) / 2 * 128)), cj = std::min(127, (int)((b.x0 + b.x1) / 2 * 128)); for (int i = 0; i < 128; i++) { float yc = (i + 0.5f) / 128; for (int j = 0; j < 128; j++) { float xc = (j + 0.5f) / 128; bool inside = (yc >= b.y0 && yc <= b.y1 && xc >= b.x0 && xc <= b.x1) || (i == ci && j == cj); if (!inside) continue; const float* pp = P + ((size_t)i * 128 + j) * 32; float s = 0.0f; for (int r = 0; r < 32; r++) s += a[r] * pp[r]; out[(size_t)i * 128 + j] = sigmoidf(s); } } return out; } // ------------------------------------------------------------------ box slots and decoder memory void memory(Model& m, const Encoded& enc, const std::vector& objects, const std::vector& texts, std::vector& M, std::vector& mvalid, std::vector& refs) { float pe = sigmoidf(m.F("pe_gate")[0]); M.assign((size_t)256 * 512, 0.0f); for (int p = 0; p < 256; p++) { int ri = p / 16, cj = p % 16; for (int half = 0; half < 2; half++) { int pos = half == 0 ? ri : cj; for (int i = 0; i < 128; i++) { double om = 1.0 / std::pow(10000.0, (double)i / 128.0); M[(size_t)p * 512 + half * 256 + i] = (float)std::sin(pos * om); M[(size_t)p * 512 + half * 256 + 128 + i] = (float)std::cos(pos * om); } } for (int c = 0; c < 512; c++) M[(size_t)p * 512 + c] = enc.E[(size_t)p * 512 + c] + pe * M[(size_t)p * 512 + c]; } mvalid.assign(enc.valid32.begin(), enc.valid32.end()); refs.clear(); const float* sp = m.F("slot_proj/kernel"); const float* sn = m.F("slot_norm/scale"); const float* emb_scale_dummy = nullptr; (void)emb_scale_dummy; CQ emb = m.Q("embedding/embedding"); std::vector row(512); for (int kind = 0; kind < 2; kind++) { const auto& boxes = kind == 0 ? objects : texts; for (size_t k = 0; k < boxes.size(); k++) { const Box& b = boxes[k]; std::vector feat(192); std::memcpy(feat.data(), enc.D.data() + ((size_t)b.ci * 64 + b.cj) * 128, sizeof(float) * 128); float bc[4] = {b.x0, b.y0, b.x1, b.y1}; for (int c = 0; c < 4; c++) for (int r2 = 0; r2 < 8; r2++) { double a = std::pow(2.0, r2) * 3.14159265358979323846 * bc[c]; feat[128 + c * 16 + r2] = (float)std::sin(a); feat[128 + c * 16 + 8 + r2] = (float)std::cos(a); } std::vector pooled(512, 0.0f); int count = 0, ci = std::min(15, (int)((b.y0 + b.y1) / 2 * 16)), cc = std::min(15, (int)((b.x0 + b.x1) / 2 * 16)); std::vector in(256, 0); for (int i = 0; i < 16; i++) for (int j = 0; j < 16; j++) { float yc = (i + 0.5f) / 16, xc = (j + 0.5f) / 16; in[i * 16 + j] = yc >= b.y0 && yc <= b.y1 && xc >= b.x0 && xc <= b.x1; } in[ci * 16 + cc] = 1; for (int p = 0; p < 256; p++) in[p] = in[p] && enc.valid32[p]; bool any = false; for (int p = 0; p < 256; p++) any = any || in[p]; if (!any) in[ci * 16 + cc] = 1; for (int p = 0; p < 256; p++) if (in[p]) { count++; for (int c = 0; c < 512; c++) pooled[c] += enc.E[(size_t)p * 512 + c]; } for (int c = 0; c < 512; c++) pooled[c] /= count; std::vector z(512, 0.0f), zp(512); gemm_nn(feat.data(), 1, 192, sp, 512, z.data()); qmm(m, "slot_pool/kernel", pooled.data(), 1, zp.data()); for (int c = 0; c < 512; c++) z[c] += zp[c]; std::vector s(512); zc_norm(z.data(), 512, sn, s.data()); int rid = m.ref_base + (kind == 0 ? (int)k : 64 + (int)k); cq_dequant_row(emb, rid, row.data()); for (int c = 0; c < 512; c++) s[c] += std::sqrt(512.0f) * row[c]; M.insert(M.end(), s.begin(), s.end()); mvalid.push_back(1); refs.push_back(rid); } } } } // namespace oasis // ==== Text side: CTC reader, decoder with caches, detokenisation namespace oasis { // ------------------------------------------------------------------ reader static void conv3x3_s(const float* in, int H, int W, int Ci, const float* w, int Co, int sh, int sw, std::vector& out, int& Ho, int& Wo) { Ho = (H - 1) / sh + 1; Wo = (W - 1) / sw + 1; out.assign((size_t)Ho * Wo * Co, 0.0f); for (int y = 0; y < Ho; y++) for (int x = 0; x < Wo; x++) { float* o = out.data() + ((size_t)y * Wo + x) * Co; for (int kh = 0; kh < 3; kh++) { int iy = y * sh - 1 + kh; if (iy < 0 || iy >= H) continue; for (int kw = 0; kw < 3; kw++) { int ix = x * sw - 1 + kw; if (ix < 0 || ix >= W) continue; const float* ip = in + ((size_t)iy * W + ix) * Ci; const float* wp = w + ((size_t)(kh * 3 + kw) * Ci) * Co; for (int ci = 0; ci < Ci; ci++) for (int co = 0; co < Co; co++) o[co] += ip[ci] * wp[(size_t)ci * Co + co]; } } } } static void dw_s(const float* in, int H, int W, int C, const float* w, int kh_, int kw_, int sh, int sw, std::vector& out, int& Ho, int& Wo) { int ph = kh_ / 2, pw_ = kw_ / 2; Ho = (H + 2 * ph - kh_) / sh + 1; Wo = (W + 2 * pw_ - kw_) / sw + 1; out.assign((size_t)Ho * Wo * C, 0.0f); for (int y = 0; y < Ho; y++) for (int x = 0; x < Wo; x++) { float* o = out.data() + ((size_t)y * Wo + x) * C; for (int a = 0; a < kh_; a++) { int iy = y * sh - ph + a; if (iy < 0 || iy >= H) continue; for (int b = 0; b < kw_; b++) { int ix = x * sw - pw_ + b; if (ix < 0 || ix >= W) continue; const float* ip = in + ((size_t)iy * W + ix) * C; const float* wp = w + (size_t)(a * kw_ + b) * C; for (int c = 0; c < C; c++) o[c] += ip[c] * wp[c]; } } } } static void norm_all(std::vector& x, int C, const float* s) { for (size_t p = 0; p < x.size() / C; p++) zc_norm(x.data() + p * C, C, s, x.data() + p * C); } // One 48 x 320 window -> argmax label per frame (80 frames). static std::vector reader_window(Model& m, const std::vector& J) { std::vector G, t, d; int H = 48, W = 320, Ho, Wo; conv3x3_s(J.data(), H, W, 3, m.F("reader/w0"), 32, 2, 2, G, Ho, Wo); H = Ho; W = Wo; norm_all(G, 32, m.F("reader/stage_norm_0/scale")); const int Cs[4] = {32, 64, 128, 384}; const int sh[3] = {2, 2, 2}, sw[3] = {2, 1, 1}; for (int j = 0; j < 3; j++) { t = G; for (auto& v : t) v = siluf(v); dw_s(t.data(), H, W, Cs[j], m.F("reader/dw_" + std::to_string(j)), 3, 3, sh[j], sw[j], d, Ho, Wo); H = Ho; W = Wo; G.assign((size_t)H * W * Cs[j + 1], 0.0f); qmm(m, "reader/pw_" + std::to_string(j) + "/kernel", d.data(), H * W, G.data()); norm_all(G, Cs[j + 1], m.F("reader/stage_norm_" + std::to_string(j + 1) + "/scale")); } // H == 3, W == 80, C == 384 std::vector Z((size_t)W * 384, 0.0f); for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) for (int c = 0; c < 384; c++) Z[(size_t)x * 384 + c] += siluf(G[((size_t)y * W + x) * 384 + c]) / H; std::vector h, mixed, vg((size_t)W * 768), gated((size_t)W * 384), dn((size_t)W * 384); for (int b = 0; b < 4; b++) { h = Z; norm_all(h, 384, m.F("reader/norm_" + std::to_string(b) + "/scale")); int ho, wo; dw_s(h.data(), 1, W, 384, m.F("reader/mix_" + std::to_string(b)), 1, 5, 1, 1, mixed, ho, wo); qmm(m, "reader/up_" + std::to_string(b) + "/kernel", mixed.data(), W, vg.data()); for (int x = 0; x < W; x++) for (int c = 0; c < 384; c++) gated[(size_t)x * 384 + c] = vg[(size_t)x * 768 + c] * siluf(vg[(size_t)x * 768 + 384 + c]); qmm(m, "reader/down_" + std::to_string(b) + "/kernel", gated.data(), W, dn.data()); for (size_t i = 0; i < Z.size(); i++) Z[i] += dn[i]; } norm_all(Z, 384, m.F("reader/final_norm/scale")); std::vector lg((size_t)W * 307); qmm(m, "reader/out/kernel", Z.data(), W, lg.data()); const float* bias = m.F("reader/out/bias"); std::vector lab(W); for (int x = 0; x < W; x++) { int best = 0; float bv = -1e30f; for (int c = 0; c < 307; c++) { float v = lg[(size_t)x * 307 + c] + bias[c]; if (v > bv) { bv = v; best = c; } } lab[x] = best; } return lab; } static void utf8_append(std::string& s, uint32_t cp) { if (cp < 0x80) s += (char)cp; else if (cp < 0x800) { s += (char)(0xC0 | (cp >> 6)); s += (char)(0x80 | (cp & 0x3F)); } else if (cp < 0x10000) { s += (char)(0xE0 | (cp >> 12)); s += (char)(0x80 | ((cp >> 6) & 0x3F)); s += (char)(0x80 | (cp & 0x3F)); } else { s += (char)(0xF0 | (cp >> 18)); s += (char)(0x80 | ((cp >> 12) & 0x3F)); s += (char)(0x80 | ((cp >> 6) & 0x3F)); s += (char)(0x80 | (cp & 0x3F)); } } std::string read_text(Model& m, const uint8_t* rgb, int W, int H, float x0, float y0, float x1, float y1) { int cx0 = (int)std::floor(x0), cy0 = (int)std::floor(y0), cx1 = (int)std::ceil(x1), cy1 = (int)std::ceil(y1); int cw = cx1 - cx0, ch = cy1 - cy0; if (cw < 1 || ch < 1) return ""; std::vector crop((size_t)cw * ch * 3, 0); for (int y = 0; y < ch; y++) for (int x = 0; x < cw; x++) { int sy = cy0 + y, sx = cx0 + x; if (sy < 0 || sx < 0 || sy >= H || sx >= W) continue; std::memcpy(&crop[((size_t)y * cw + x) * 3], &rgb[((size_t)sy * W + sx) * 3], 3); } int U = std::max(1, (int)std::nearbyint(48.0 * cw / ch)); std::vector strip = resize_rgb(crop.data(), cw, ch, U, 48); std::vector starts; if (U <= 320) starts.push_back(0); else for (int s = 0; s < U - 320 + 223; s += 224) starts.push_back(s); int n_frames = (U + 3) / 4; std::string out; int prev = 0; for (int xs : starts) { std::vector J((size_t)48 * 320 * 3, 127.0f / 127.5f - 1.0f); for (int y = 0; y < 48; y++) for (int x = 0; x < 320 && xs + x < U; x++) for (int c = 0; c < 3; c++) J[((size_t)y * 320 + x) * 3 + c] = strip[((size_t)y * U + xs + x) * 3 + c] / 127.5f - 1.0f; std::vector lab = reader_window(m, J); int lo = xs == 0 ? 0 : 12; int hi = xs + 320 >= U ? (U - xs + 3) / 4 : 68; for (int t = lo; t < std::min(hi, 80); t++) { if (xs / 4 + t >= n_frames) break; int l = lab[t]; if (l != 0 && l != prev) utf8_append(out, m.charset[l - 1]); prev = l; } } return out; } // ------------------------------------------------------------------ decoder static const int ORDERS[2] = {2, 3}; static const uint32_t SEED = 0x9E3779B9u, PRIME = 0x01000193u; static uint32_t engram_index(const std::vector& toks, int t, int oi, int order, int h) { uint32_t acc = SEED * (uint32_t)(oi * 2 + h + 1); for (int j = 0; j < order; j++) { uint32_t tok = t - j >= 0 ? (uint32_t)toks[t - j] : 0u; acc = (acc ^ tok) * PRIME; } acc ^= acc >> 15; return acc % 18432u; } static inline float fexp(float x) { x = x < -87.0f ? -87.0f : x; float t = x * 1.4426950408889634f, fi = std::floor(t), f = t - fi; float p = 1.0f + f * (0.69314718f + f * (0.24022651f + f * (0.05550411f + f * (0.00961813f + f * (0.00133336f + f * 0.00015404f))))); int32_t bits = ((int32_t)fi + 127) << 23; float sc; std::memcpy(&sc, &bits, 4); return p * sc; } // 4 x 4 Sinkhorn: twenty rounds of row then column normalisation, multiplicative form. static void sinkhorn(float* L) { double K[16]; float mx = L[0]; for (int i = 1; i < 16; i++) mx = std::max(mx, L[i]); for (int i = 0; i < 16; i++) K[i] = std::exp((double)L[i] - mx); for (int it = 0; it < 20; it++) { for (int i = 0; i < 4; i++) { double s = K[i * 4] + K[i * 4 + 1] + K[i * 4 + 2] + K[i * 4 + 3]; for (int j = 0; j < 4; j++) K[i * 4 + j] /= s; } for (int j = 0; j < 4; j++) { double s = K[j] + K[4 + j] + K[8 + j] + K[12 + j]; for (int i = 0; i < 4; i++) K[i * 4 + j] /= s; } } for (int i = 0; i < 16; i++) L[i] = (float)K[i]; } #if defined(OASIS_NEON) static inline uint16_t to_half(float f) { __fp16 h = (__fp16)f; uint16_t b; std::memcpy(&b, &h, 2); return b; } #endif struct Decoder { Model& m; int nM; std::vector mvalid; // Image-memory keys and values, shared by copies. On Arm, whose caches are small, they are kept in fp16, // which halves what each token reads, and laid out in the order a head reads them: keys per head as // [nMp / 16][48][16] (memory entries padded to nMp), values per head as [nM][64]. struct Cross { std::vector k[8], v[8]; std::vector kt[8], v16[8]; }; std::shared_ptr cross = std::make_shared(); int nMp = 0; std::vector* ck = cross->k; std::vector* cv = cross->v; std::vector toks; // Self-attention keys and values of every position so far: [t][96] and [t][128], both groups interleaved; // on Arm one array per group, [t][48] and [t][64], so that a head reads its group's history in one run. #if defined(OASIS_NEON) std::vector K[8][2], V[8][2]; std::vector raw[8][3]; #else std::vector K[8], V[8], raw[8][3]; #endif std::vector ev[2]; CQ emb; struct Layer { CQ q, k, v, gate, out, cq_, cgate, cout; const float *taps[3], *qn, *kn, *pre, *post, *cross, *pcross, *cqn, *hada; float ag, cg; const float *d1, *d2, *d3, *d4, *b2, *cond_v, *cond_u, *w1a, *w1b, *w2a, *w2b, *w3a, *w3b; } Ls[8]; struct Site { CQ tab, key, val; const float* taps; } sites[2]; const float *phi_pre, *phi_post, *phi_res, *a_pre, *a_post, *a_res, *b_pre, *b_post, *b_res, *final_norm; QX qx; SpinPool& pool; std::string L(const char* name, int l) { return std::string("stack/layers/block/") + name + "/" + std::to_string(l); } // y_i = W_i x for several quantised matrices of one width, x already prepared in qx; rows are shared out. void rows(std::initializer_list mats, std::initializer_list outs, int chunk = 32) { const CQ* q[4]; float* y[4]; int off[5] = {0}, n = 0; for (const CQ* p : mats) q[n++] = p; n = 0; for (float* p : outs) y[n++] = p; for (int i = 0; i < n; i++) off[i + 1] = off[i] + q[i]->rows; #if defined(OASIS_NEON) // An Arm core reads a long run of rows far faster than scattered pieces of a few kilobytes, so each // thread claims one contiguous share of the rows; the result does not depend on how rows are shared. chunk = std::max(chunk, (off[n] + pool.size() - 1) / pool.size()); #endif const CbTable t = cb_table(*q[0]); pool.run(off[n], chunk, [&](int g) { int k = 0; while (g >= off[k + 1]) k++; y[k][g - off[k]] = cq_row(*q[k], qx, g - off[k], t); }); } Decoder(Model& mm, const std::vector& M, const std::vector& mv, SpinPool& p) : m(mm), mvalid(mv), pool(p) { nM = (int)(M.size() / 512); emb = m.Q("embedding/embedding"); for (int l = 0; l < 8; l++) { Layer& y = Ls[l]; y.q = m.Q(L("self_attn/q_proj/kernel", l)); y.k = m.Q(L("self_attn/k_proj/kernel", l)); y.v = m.Q(L("self_attn/v_proj/kernel", l)); y.gate = m.Q(L("self_attn/gate_proj/kernel", l)); y.out = m.Q(L("self_attn/out_proj/kernel", l)); y.cq_ = m.Q(L("cross_attn/q_proj/kernel", l)); y.cgate = m.Q(L("cross_attn/gate_proj/kernel", l)); y.cout = m.Q(L("cross_attn/out_proj/kernel", l)); y.taps[0] = m.F(L("self_attn/q_taps", l)); y.taps[1] = m.F(L("self_attn/k_taps", l)); y.taps[2] = m.F(L("self_attn/v_taps", l)); y.qn = m.F(L("self_attn/q_norm/scale", l)); y.kn = m.F(L("self_attn/k_norm/scale", l)); y.pre = m.F(L("ZCRMSNorm_0/scale", l)); y.post = m.F(L("post_attn_norm/scale", l)); y.cross = m.F(L("cross_norm/scale", l)); y.pcross = m.F(L("post_cross_norm/scale", l)); y.cqn = m.F(L("cross_attn/q_norm/scale", l)); y.hada = m.F(L("pre_hada_norm/scale", l)); y.ag = sigmoidf(m.F(L("attn_gate", l))[0]); y.cg = sigmoidf(m.F(L("cross_gate", l))[0]); auto P = [&](const char* n) { return m.F(L((std::string("hadamard_mlp/") + n).c_str(), l)); }; y.d1 = P("d1"); y.d2 = P("d2"); y.d3 = P("d3"); y.d4 = P("d4"); y.b2 = P("b2"); y.cond_v = P("cond_v"); y.cond_u = P("cond_u"); y.w1a = P("w1a"); y.w1b = P("w1b"); y.w2a = P("w2a"); y.w2b = P("w2b"); y.w3a = P("w3a"); y.w3b = P("w3b"); ck[l].resize((size_t)nM * 384); cv[l].resize((size_t)nM * 512); qmm(m, L("cross_attn/k_proj/kernel", l), M.data(), nM, ck[l].data()); qmm(m, L("cross_attn/v_proj/kernel", l), M.data(), nM, cv[l].data()); const float* kn = m.F(L("cross_attn/k_norm/scale", l)); for (int j = 0; j < nM; j++) for (int h = 0; h < 8; h++) zc_norm(&ck[l][(size_t)j * 384 + h * 48], 48, kn, &ck[l][(size_t)j * 384 + h * 48]); #if defined(OASIS_NEON) nMp = (nM + 15) / 16 * 16; cross->kt[l].assign((size_t)8 * 48 * nMp, 0); for (int j = 0; j < nM; j++) for (int h = 0; h < 8; h++) for (int c = 0; c < 48; c++) cross->kt[l][(size_t)h * 48 * nMp + (size_t)(j / 16) * 768 + c * 16 + j % 16] = to_half(ck[l][(size_t)j * 384 + h * 48 + c]); cross->v16[l].resize((size_t)nM * 512); for (int h = 0; h < 8; h++) for (int j = 0; j < nM; j++) for (int c = 0; c < 64; c++) cross->v16[l][((size_t)h * nM + j) * 64 + c] = to_half(cv[l][(size_t)j * 512 + h * 64 + c]); std::vector().swap(ck[l]); std::vector().swap(cv[l]); #endif } for (int s = 0; s < 2; s++) { std::string pre = "engrams_" + std::to_string(s) + "/"; sites[s] = {m.Q(pre + "embedding"), m.Q(pre + "key_proj/kernel"), m.Q(pre + "value_proj/kernel"), m.F(pre + "taps")}; } phi_pre = m.Wdq("stack/mhc_phi_pre"); phi_post = m.Wdq("stack/mhc_phi_post"); phi_res = m.Wdq("stack/mhc_phi_res"); a_pre = m.F("stack/mhc_a_pre"); a_post = m.F("stack/mhc_a_post"); a_res = m.F("stack/mhc_a_res"); b_pre = m.F("stack/mhc_b_pre"); b_post = m.F("stack/mhc_b_post"); b_res = m.F("stack/mhc_b_res"); final_norm = m.F("stack/final_norm/scale"); } static void kron(const float* in, const float* a, const float* b, float* o) { #if defined(OASIS_NEON) // tmp = in [16, 32] times b [32, 32], then o = a^T [16, 16] times tmp, a row of 32 at a time in // registers; every output adds its products in the same order as the loops below. float tmp[16 * 32]; for (int i = 0; i < 16; i++) { float32x4_t c[8]; for (int q = 0; q < 8; q++) c[q] = vdupq_n_f32(0.0f); for (int j = 0; j < 32; j++) { const float z = in[i * 32 + j]; const float* br = b + j * 32; for (int q = 0; q < 8; q++) c[q] = vfmaq_n_f32(c[q], vld1q_f32(br + 4 * q), z); } for (int q = 0; q < 8; q++) vst1q_f32(tmp + i * 32 + 4 * q, c[q]); } for (int k = 0; k < 16; k++) { float32x4_t c[8]; for (int q = 0; q < 8; q++) c[q] = vdupq_n_f32(0.0f); for (int i = 0; i < 16; i++) { const float av = a[i * 16 + k]; const float* tr = tmp + i * 32; for (int q = 0; q < 8; q++) c[q] = vfmaq_n_f32(c[q], vld1q_f32(tr + 4 * q), av); } for (int q = 0; q < 8; q++) vst1q_f32(o + k * 32 + 4 * q, c[q]); } #else float tmp[16 * 32] = {0}; for (int i = 0; i < 16; i++) for (int j = 0; j < 32; j++) { float z = in[i * 32 + j]; const float* br = b + j * 32; for (int l2 = 0; l2 < 32; l2++) tmp[i * 32 + l2] += z * br[l2]; } for (int k = 0; k < 16 * 32; k++) o[k] = 0.0f; for (int i = 0; i < 16; i++) for (int k = 0; k < 16; k++) { float av = a[i * 16 + k]; for (int l2 = 0; l2 < 32; l2++) o[k * 32 + l2] += av * tmp[i * 32 + l2]; } #endif } void hmlp(const Layer& y, const float* x, float* out) { float c8[8], z[512], t[512]; float mx = -1e30f; for (int r = 0; r < 8; r++) c8[r] = 0.0f; for (int i = 0; i < 512; i++) { const float* row = y.cond_v + (size_t)i * 8; for (int r = 0; r < 8; r++) c8[r] += x[i] * row[r]; } for (int r = 0; r < 8; r++) mx = std::max(mx, c8[r]); float den = 0; for (int r = 0; r < 8; r++) { c8[r] = std::exp(c8[r] - mx); den += c8[r]; } float cond[512]; for (int i = 0; i < 512; i++) cond[i] = 1.0f; for (int r = 0; r < 8; r++) { float w = c8[r] / den; const float* u = y.cond_u + (size_t)r * 512; for (int i = 0; i < 512; i++) cond[i] += w * u[i]; } for (int i = 0; i < 512; i++) t[i] = y.d1[i] * x[i]; kron(t, y.w1a, y.w1b, z); for (int i = 0; i < 512; i++) t[i] = z[m.perm1[i]]; #if defined(OASIS_NEON) for (int i = 0; i < 512; i++) z[i] = y.d2[i] * cond[i] * t[i] + y.b2[i]; silu_inplace(z, 512); #else for (int i = 0; i < 512; i++) z[i] = siluf(y.d2[i] * cond[i] * t[i] + y.b2[i]); #endif kron(z, y.w2a, y.w2b, t); for (int i = 0; i < 512; i++) z[i] = t[m.perm2[i]]; for (int i = 0; i < 512; i++) t[i] = y.d3[i] * z[i]; kron(t, y.w3a, y.w3b, z); for (int i = 0; i < 512; i++) out[i] = y.d4[i] * z[i]; } void softmax_rows(float* s, int n) { float mx = -1e30f; for (int j = 0; j < n; j++) mx = std::max(mx, s[j]); float den = 0.0f; for (int j = 0; j < n; j++) { s[j] = fexp(s[j] - mx); den += s[j]; } float inv = 1.0f / den; for (int j = 0; j < n; j++) s[j] *= inv; } void block(int l, const float* u, int t, float* xout) { const Layer& y = Ls[l]; float x[512], xn[512], tmp[512]; std::memcpy(x, u, sizeof x); int site = l == 3 ? 0 : l == 7 ? 1 : -1; if (site >= 0) { const Site& st = sites[site]; float e[512]; for (int oi = 0; oi < 2; oi++) for (int h = 0; h < 2; h++) { int ti = oi * 2 + h; if (t - (ORDERS[oi] - 1) < 0) std::memset(e + ti * 128, 0, 128 * sizeof(float)); else cq_dequant_row(st.tab, ti * 18432 + (int)engram_index(toks, t, oi, ORDERS[oi], h), e + ti * 128); } float k[512], vr[512], v[512] = {0}; prep_x(e, 512, 2, qx); rows({&st.key, &st.val}, {k, vr}); ev[site].insert(ev[site].end(), vr, vr + 512); for (int j = 0; j < 4; j++) { int tt = t - 3 * j; if (tt < 0) continue; const float* tp = st.taps + j * 512; const float* src = ev[site].data() + (size_t)tt * 512; for (int c = 0; c < 512; c++) v[c] += tp[c] * src[c]; } float ru[512], rk[512]; rms_unit(x, 512, ru); rms_unit(k, 512, rk); float alpha = sigmoidf(dotf(ru, rk, 512) / std::sqrt(512.0f)); for (int c = 0; c < 512; c++) x[c] += alpha * v[c]; } float skip[512]; std::memcpy(skip, x, sizeof skip); zc_norm(x, 512, y.pre, xn); prep_x(xn, 512, 2, qx); const int dims[3] = {384, 96, 128}; float qkv[3][384], proj[3][384]; float gate[512]; rows({&y.gate, &y.q, &y.k, &y.v}, {gate, proj[0], proj[1], proj[2]}); for (int s = 0; s < 3; s++) { const float* r = proj[s]; raw[l][s].insert(raw[l][s].end(), r, r + dims[s]); const float* taps = y.taps[s]; for (int c = 0; c < dims[s]; c++) { float a = 0; for (int j = 0; j < 3; j++) if (t - j >= 0) a += taps[j * dims[s] + c] * raw[l][s][(size_t)(t - j) * dims[s] + c]; qkv[s][c] = a; } } auto rope48 = [&](float* v) { float a[24], b[24]; std::memcpy(a, v, sizeof a); std::memcpy(b, v + 24, sizeof b); for (int i = 0; i < 24; i++) { float c = rope_c[(size_t)t * 24 + i], s = rope_s[(size_t)t * 24 + i]; v[i] = a[i] * c - b[i] * s; v[24 + i] = b[i] * c + a[i] * s; } }; for (int h = 0; h < 8; h++) { zc_norm(qkv[0] + h * 48, 48, y.qn, qkv[0] + h * 48); rope48(qkv[0] + h * 48); } for (int h = 0; h < 2; h++) { zc_norm(qkv[1] + h * 48, 48, y.kn, qkv[1] + h * 48); rope48(qkv[1] + h * 48); } #if defined(OASIS_NEON) for (int g = 0; g < 2; g++) { K[l][g].insert(K[l][g].end(), qkv[1] + g * 48, qkv[1] + g * 48 + 48); V[l][g].insert(V[l][g].end(), qkv[2] + g * 64, qkv[2] + g * 64 + 64); } const float* kb[2] = {K[l][0].data(), K[l][1].data()}; const float* vb[2] = {V[l][0].data(), V[l][1].data()}; const size_t ks = 48, vs = 64; #else K[l].insert(K[l].end(), qkv[1], qkv[1] + 96); V[l].insert(V[l].end(), qkv[2], qkv[2] + 128); #endif int n = t + 1; float o[512]; const int span = std::max(n, std::max(nM, nMp)); scratch.resize((size_t)8 * span); const float isq = 1.0f / std::sqrt(48.0f); // Heads are independent; each keeps its own score row. pool.run(8, n >= 64 ? 1 : 8, [&](int h) { float* s = scratch.data() + (size_t)h * span; int g = h / 4; #if defined(OASIS_NEON) for (int j = 0; j < n; j++) s[j] = dotf(qkv[0] + h * 48, kb[g] + (size_t)j * ks, 48) * isq; #else for (int j = 0; j < n; j++) s[j] = dotf(qkv[0] + h * 48, &K[l][(size_t)j * 96 + g * 48], 48) * isq; #endif softmax_rows(s, n); float* oh = o + h * 64; for (int c = 0; c < 64; c++) oh[c] = 0; for (int j = 0; j < n; j++) { #if defined(OASIS_NEON) const float* vv = vb[g] + (size_t)j * vs; #else const float* vv = &V[l][(size_t)j * 128 + g * 64]; #endif float w = s[j]; for (int c = 0; c < 64; c++) oh[c] += w * vv[c]; } }); #if defined(OASIS_NEON) mul_sigmoid(o, gate, 512); #else for (int c = 0; c < 512; c++) o[c] *= sigmoidf(gate[c]); #endif float A[512]; prep_x(o, 512, 2, qx); rows({&y.out}, {A}); zc_norm(A, 512, y.post, tmp); for (int c = 0; c < 512; c++) x[c] = skip[c] + y.ag * tmp[c]; std::memcpy(skip, x, sizeof skip); float xc[512], qc[384]; zc_norm(x, 512, y.cross, xc); prep_x(xc, 512, 2, qx); rows({&y.cq_, &y.cgate}, {qc, gate}); for (int h = 0; h < 8; h++) zc_norm(qc + h * 48, 48, y.cqn, qc + h * 48); #if defined(OASIS_NEON) // Scores for sixteen memory entries at a time from the transposed fp16 keys, then the fp16 values. pool.run(8, nM >= 64 ? 1 : 8, [&](int h) { float* s = scratch.data() + (size_t)h * span; const uint16_t* kh = cross->kt[l].data() + (size_t)h * 48 * nMp; const float* qh = qc + h * 48; for (int j0 = 0; j0 < nMp; j0 += 16) { float32x4_t a0 = vdupq_n_f32(0.0f), a1 = a0, a2 = a0, a3 = a0; for (int c = 0; c < 48; c++) { const uint16_t* kr = kh + (size_t)(j0 / 16) * 768 + c * 16; const float16x8_t k0 = vreinterpretq_f16_u16(vld1q_u16(kr)), k1 = vreinterpretq_f16_u16(vld1q_u16(kr + 8)); const float qv = qh[c]; a0 = vfmaq_n_f32(a0, vcvt_f32_f16(vget_low_f16(k0)), qv); a1 = vfmaq_n_f32(a1, vcvt_high_f32_f16(k0), qv); a2 = vfmaq_n_f32(a2, vcvt_f32_f16(vget_low_f16(k1)), qv); a3 = vfmaq_n_f32(a3, vcvt_high_f32_f16(k1), qv); } vst1q_f32(s + j0, a0); vst1q_f32(s + j0 + 4, a1); vst1q_f32(s + j0 + 8, a2); vst1q_f32(s + j0 + 12, a3); } for (int j = 0; j < nM; j++) s[j] = mvalid[j] ? s[j] * isq : -1e30f; softmax_rows(s, nM); float32x4_t acc[16]; for (int q = 0; q < 16; q++) acc[q] = vdupq_n_f32(0.0f); const uint16_t* vh = cross->v16[l].data() + (size_t)h * nM * 64; for (int j = 0; j < nM; j++) { if (!mvalid[j]) continue; const float w = s[j]; const uint16_t* vv = vh + (size_t)j * 64; for (int q = 0; q < 8; q++) { const float16x8_t v8 = vreinterpretq_f16_u16(vld1q_u16(vv + q * 8)); acc[2 * q] = vfmaq_n_f32(acc[2 * q], vcvt_f32_f16(vget_low_f16(v8)), w); acc[2 * q + 1] = vfmaq_n_f32(acc[2 * q + 1], vcvt_high_f32_f16(v8), w); } } for (int q = 0; q < 16; q++) vst1q_f32(o + h * 64 + q * 4, acc[q]); }); #else pool.run(8, nM >= 64 ? 1 : 8, [&](int h) { float* s = scratch.data() + (size_t)h * span; for (int j = 0; j < nM; j++) { if (!mvalid[j]) { s[j] = -1e30f; continue; } s[j] = dotf(qc + h * 48, &ck[l][(size_t)j * 384 + h * 48], 48) * isq; } softmax_rows(s, nM); float* oh = o + h * 64; for (int c = 0; c < 64; c++) oh[c] = 0; for (int j = 0; j < nM; j++) { if (!mvalid[j]) continue; const float* vv = &cv[l][(size_t)j * 512 + h * 64]; float w = s[j]; for (int c = 0; c < 64; c++) oh[c] += w * vv[c]; } }); #endif #if defined(OASIS_NEON) mul_sigmoid(o, gate, 512); #else for (int c = 0; c < 512; c++) o[c] *= sigmoidf(gate[c]); #endif prep_x(o, 512, 2, qx); rows({&y.cout}, {A}); zc_norm(A, 512, y.pcross, tmp); for (int c = 0; c < 512; c++) x[c] = skip[c] + y.cg * tmp[c]; zc_norm(x, 512, y.hada, xn); hmlp(y, xn, tmp); for (int c = 0; c < 512; c++) xout[c] = x[c] + tmp[c]; } std::vector scratch, rope_c, rope_s; void ensure_rope(int t) { int have = (int)(rope_c.size() / 24); if (t < have) return; int want = std::max(t + 1, have * 2 + 64); rope_c.resize((size_t)want * 24); rope_s.resize((size_t)want * 24); for (int p = have; p < want; p++) for (int i = 0; i < 24; i++) { double fr = 1.0 / std::pow(100000.0, (2.0 * i) / 48.0); rope_c[(size_t)p * 24 + i] = (float)std::cos(p * fr); rope_s[(size_t)p * 24 + i] = (float)std::sin(p * fr); } } // Feed one token; returns logits over the vocabulary. std::vector step(int tok) { int t = (int)toks.size(); toks.push_back(tok); ensure_rope(t); float X[4][512], row[512]; cq_dequant_row(emb, tok, row); for (int ln = 0; ln < 4; ln++) for (int c = 0; c < 512; c++) X[ln][c] = std::sqrt(512.0f) * row[c]; for (int l = 0; l < 8; l++) { float nx[2048]; rms_unit(&X[0][0], 2048, nx); float hv[24]; #if defined(OASIS_NEON) const int phi_chunk = 8; // a contiguous share of the projections for each of three threads pool.run(24, phi_chunk, [&](int i) { #else pool.run(24, 4, [&](int i) { #endif const float* row = i < 4 ? phi_pre + (size_t)(4 * l + i) * 2048 : i < 8 ? phi_post + (size_t)(4 * l + i - 4) * 2048 : phi_res + (size_t)(16 * l + i - 8) * 2048; hv[i] = dotf(row, nx, 2048); }); float hp[4], hq[4], hr[16]; for (int i = 0; i < 4; i++) { hp[i] = hv[i]; hq[i] = hv[4 + i]; } for (int i = 0; i < 16; i++) hr[i] = hv[8 + i]; float u[512] = {0}, yv[512]; for (int i = 0; i < 4; i++) { float off = (l % 4 == i) ? 4.0f : -4.0f; hp[i] = sigmoidf(a_pre[l] * hp[i] + b_pre[l * 4 + i] + off); hq[i] = 2.0f * sigmoidf(a_post[l] * hq[i] + b_post[l * 4 + i] + ((l % 4 == i) ? 0.0f : -4.0f)); for (int c = 0; c < 512; c++) u[c] += hp[i] * X[i][c]; } for (int i = 0; i < 16; i++) hr[i] = a_res[l] * hr[i] + b_res[l * 16 + i]; sinkhorn(hr); block(l, u, t, yv); for (int c = 0; c < 512; c++) yv[c] -= u[c]; float Xn[4][512]; for (int i = 0; i < 4; i++) for (int c = 0; c < 512; c++) Xn[i][c] = hq[i] * yv[c] + hr[i * 4] * X[0][c] + hr[i * 4 + 1] * X[1][c] + hr[i * 4 + 2] * X[2][c] + hr[i * 4 + 3] * X[3][c]; std::memcpy(X, Xn, sizeof X); } float h[512], hn[512]; for (int c = 0; c < 512; c++) h[c] = 0.25f * (X[0][c] + X[1][c] + X[2][c] + X[3][c]); zc_norm(h, 512, final_norm, hn); std::vector logits(emb.rows); prep_x(hn, 512, emb.bits, qx); rows({&emb}, {logits.data()}, 128); return logits; } }; static int decode_threads() { return std::max(1, std::min(engine_threads(), 8)); } // Keeps the pool's workers spinning for the length of one decode. struct Awake { SpinPool& p; explicit Awake(SpinPool& pool) : p(pool) { p.wake(); } ~Awake() { p.sleep(); } }; // When the sequence ends in one short block written several times over, which the decoder only does // once it has fallen into a loop (a block of 2 to 8 tokens four times, or one token six times), returns // how many trailing tokens repeat the first copy; otherwise 0. static int looping(const std::vector& s) { const int n = (int)s.size(); for (int p = 1; p <= 8; p++) { int reps = p == 1 ? 6 : 4; if (n < p * reps) continue; bool same = true; for (int k = 1; k < reps && same; k++) for (int i = 0; i < p && same; i++) same = s[n - 1 - i] == s[n - 1 - i - k * p]; if (same) return (reps - 1) * p; } return 0; } // Longest reasoning trace kept; a trace that has not closed by then is closed by the engine. static const int TRACE_MAX = 256; // Keyword biasing: a small logit bonus for a keyword's opening word piece, and a larger one for each // following piece once the earlier ones have been written, so a keyword the model starts is completed // without common word starts being pushed into every line. static const float KEYWORD_START_BONUS = 1.0f; static const float KEYWORD_NEXT_BONUS = 3.0f; std::vector generate(Model& m, const std::vector& M, const std::vector& mvalid, const std::vector& refs, const std::string& language, int max_tokens, const std::vector& keywords) { std::lock_guard lock(m.decode_mutex); if (!m.pool) m.pool = std::make_shared(decode_threads()); Decoder dec(m, M, mvalid, *m.pool); Awake awake(*m.pool); std::vector banned(m.vocab, 0); for (auto& kv : m.langs) banned[kv.second] = 1; for (int i = m.ref_base; i < m.vocab; i++) banned[i] = 1; for (int r : refs) banned[r] = 0; auto it = m.langs.find(language); int lang = it != m.langs.end() ? it->second : m.langs.at("en"); // A keyword follows a space in names and the trace, or "=" in attribute values; each spelling is its // own token sequence. Only the spaced spelling, which opens on a word start, gets the opening bonus. std::vector, bool>> seqs; for (const auto& k : keywords) for (int spaced = 1; spaced >= 0; spaced--) { std::vector s = encode_text(m, spaced ? " " + k : k); bool dup = false; for (const auto& q : seqs) dup = dup || q.first == s; if (!s.empty() && !dup) seqs.push_back({s, spaced == 1}); } int think = -1, think_end = -1; for (const auto& mk : m.markers) { if (mk.first == "") think = mk.second; if (mk.first == "") think_end = mk.second; } std::vector logits = dec.step(m.bos); logits = dec.step(lang); std::vector out; std::vector> bonus; bool in_trace = false; int trace_len = 0; for (int s = 0; s < max_tokens; s++) { bonus.clear(); for (const auto& [q, opens] : seqs) { int n = std::min((int)q.size() - 1, (int)out.size()); for (int len = n; len >= 0; len--) if (std::equal(q.begin(), q.begin() + len, out.end() - len)) { if (!len && !opens) break; float add = len ? KEYWORD_NEXT_BONUS : KEYWORD_START_BONUS; bool have = false; for (auto& b : bonus) if (b.first == q[len]) { b.second = std::max(b.second, add); have = true; } if (!have) bonus.push_back({q[len], add}); break; } } for (const auto& b : bonus) logits[b.first] += b.second; int best = -1; float bv = -1e30f; for (int i = 0; i < (int)logits.size(); i++) if (!banned[i] && logits[i] > bv) { bv = logits[i]; best = i; } if (best < 0 || best == m.eos) break; // A trace that loops or runs on is closed so that the description still gets written; a // description that loops ends where the loop began to repeat. Repeated copies are dropped. out.push_back(best); if (best == think && think >= 0) { in_trace = true; } else if (in_trace) { int rep = looping(out); if (best == think_end) { in_trace = false; } else if (++trace_len > TRACE_MAX || rep) { out.resize(out.size() - std::max(rep, 1)); // the newest token was never fed to the decoder out.push_back(think_end); in_trace = false; } } else if (int rep = looping(out)) { out.resize(out.size() - rep); break; } logits = dec.step(out.back()); } return out; } // BPE over one marker-free segment: adjacent symbols merged lowest piece id first (piece scores fall // with id, so this is SentencePiece's highest-score-first order), unknown symbols as bytes. static void bpe(const Model& m, const std::string& seg, std::vector& ids) { std::vector syms; for (size_t i = 0; i < seg.size();) { unsigned char c = (unsigned char)seg[i]; size_t n = c < 0x80 ? 1 : (c >> 5) == 6 ? 2 : (c >> 4) == 14 ? 3 : (c >> 3) == 30 ? 4 : 1; if (i + n > seg.size()) n = 1; syms.push_back(seg.substr(i, n)); i += n; } for (;;) { int best = INT_MAX; size_t at = SIZE_MAX; for (size_t i = 0; i + 1 < syms.size(); i++) { auto it = m.piece_id.find(syms[i] + syms[i + 1]); if (it != m.piece_id.end() && it->second < best) { best = it->second; at = i; } } if (at == SIZE_MAX) break; syms[at] += syms[at + 1]; syms.erase(syms.begin() + (long)at + 1); } for (const auto& s : syms) { auto it = m.piece_id.find(s); if (it != m.piece_id.end()) ids.push_back(it->second); else for (unsigned char b : s) ids.push_back(m.byte_id[b]); } } // SentencePiece encoding as the vocabulary specifies it: spaces become U+2581, no dummy prefix, // user-defined pieces such as or language tags taken whole, the text between them by BPE. std::vector encode_text(const Model& m, const std::string& text) { std::string esc; for (char c : text) { if (c == ' ') esc += "\xE2\x96\x81"; else esc += c; } std::vector ids; size_t i = 0, start = 0; while (i < esc.size()) { const std::pair* hit = nullptr; for (const auto& mk : m.markers) if (esc.compare(i, mk.first.size(), mk.first) == 0) { hit = &mk; break; } if (!hit) { i++; continue; } bpe(m, esc.substr(start, i - start), ids); ids.push_back(hit->second); i += hit->first.size(); start = i; } bpe(m, esc.substr(start), ids); return ids; } static std::string json_str(const std::string& s) { std::string o = "\""; for (unsigned char c : s) { if (c == '"') o += "\\\""; else if (c == '\\') o += "\\\\"; else if (c == '\n') o += "\\n"; else if (c < 0x20) { char b[8]; std::snprintf(b, sizeof b, "\\u%04x", c); o += b; } else o += (char)c; } return o + "\""; } static std::string json_num(double v) { if (!std::isfinite(v)) return "null"; char b[32]; std::snprintf(b, sizeof b, "%.6g", v); return b; } // Log-probability of tok under the softmax over the tokens not in banned. static double log_prob(const std::vector& L, const std::vector& banned, int tok) { if (banned[tok]) return -INFINITY; float mx = -1e30f; for (size_t i = 0; i < L.size(); i++) if (!banned[i] && L[i] > mx) mx = L[i]; double s = 0.0; for (size_t i = 0; i < L.size(); i++) if (!banned[i]) s += std::exp((double)L[i] - mx); return (double)L[tok] - mx - std::log(s); } // A form is one request line naming every field, then one answer line per field: // ? sign=stop|yield; n#0..3; note$60; where@sign // sign=stop // n=1 ... // Choices and counts are scored as complete answers with their terminator (a newline, or after the // last field), choices with "-" for abstention; text is decoded greedily; a box is a softmax over the // occupied reference tokens. Each answer is written into the context before the next field. std::string fill_form(Model& m, const std::vector& M, const std::vector& mvalid, const std::vector& refs, const std::vector>& ref_boxes, const std::string& language, const std::vector& form) { std::lock_guard lock(m.decode_mutex); if (!m.pool) m.pool = std::make_shared(decode_threads()); Decoder dec(m, M, mvalid, *m.pool); Awake awake(*m.pool); std::vector banned(m.vocab, 0), no_refs(m.vocab, 0); for (auto& kv : m.langs) banned[kv.second] = no_refs[kv.second] = 1; for (int i = m.ref_base; i < m.vocab; i++) banned[i] = no_refs[i] = 1; for (int r : refs) banned[r] = 0; auto lit = m.langs.find(language); int lang = lit != m.langs.end() ? lit->second : m.langs.at("en"); std::string line = "? "; for (size_t i = 0; i < form.size(); i++) { const FormField& f = form[i]; if (i) line += "; "; line += f.name; if (f.kind == FormField::CHOICE) { line += "="; for (size_t c = 0; c < f.choices.size(); c++) line += (c ? "|" : "") + f.choices[c]; } else if (f.kind == FormField::COUNT) { line += "#" + std::to_string(f.lo) + ".." + std::to_string(f.hi); } else if (f.kind == FormField::TEXT) { line += "$" + std::to_string(f.max_length); } else { line += "@" + f.of; } } std::vector prompt = {m.bos, lang}; for (int t : encode_text(m, line + "\n")) prompt.push_back(t); if (prompt.size() > 1024) throw std::runtime_error("form exceeds the decoder context"); std::vector logits; for (int t : prompt) logits = dec.step(t); const std::vector newline = encode_text(m, "\n"); std::ostringstream js; js << "{"; for (size_t i = 0; i < form.size(); i++) { const FormField& f = form[i]; const bool last = i + 1 == form.size(); const std::vector term = last ? std::vector{m.eos} : newline; for (int t : encode_text(m, f.name + "=")) logits = dec.step(t); js << (i ? "," : "") << json_str(f.name) << ":"; std::vector answer; // tokens written into the context after "name=" if (f.kind == FormField::CHOICE || f.kind == FormField::COUNT) { std::vector cands; if (f.kind == FormField::CHOICE) { cands = f.choices; cands.push_back("-"); } else { for (long v = f.lo; v <= f.hi; v++) cands.push_back(std::to_string(v)); } std::vector> toks(cands.size()); std::vector score(cands.size()); for (size_t c = 0; c < cands.size(); c++) { toks[c] = encode_text(m, cands[c]); toks[c].insert(toks[c].end(), term.begin(), term.end()); Decoder d = dec; std::vector L = logits; double s = 0.0; for (size_t k = 0; k < toks[c].size(); k++) { s += log_prob(L, banned, toks[c][k]); if (k + 1 < toks[c].size()) L = d.step(toks[c][k]); } score[c] = s; } double mx = -INFINITY, den = 0.0; size_t best = 0; for (size_t c = 0; c < cands.size(); c++) if (score[c] > mx) { mx = score[c]; best = c; } for (double s : score) den += std::exp(s - mx); if (f.kind == FormField::CHOICE) js << "{\"value\":" << (best == f.choices.size() ? "null" : f.values[best]); else js << "{\"value\":" << (f.lo + (long)best); js << ",\"probabilities\":{"; for (size_t c = 0; c < cands.size(); c++) js << (c ? "," : "") << json_str(cands[c]) << ":" << json_num(std::exp(score[c] - mx) / den); js << "}}"; answer = toks[best]; } else if (f.kind == FormField::TEXT) { std::vector gen; double s = 0.0; bool ended = false; for (int step = 0; step < 4 * f.max_length + 8; step++) { int tok = -1; float bv = -1e30f; for (int v = 0; v < (int)logits.size(); v++) if (!no_refs[v] && logits[v] > bv) { bv = logits[v]; tok = v; } s += log_prob(logits, no_refs, tok); if (tok == term[0] || tok == m.eos || tok == newline[0]) { ended = true; break; } gen.push_back(tok); logits = dec.step(tok); std::string sofar = detokenize(m, gen); size_t chars = 0; for (unsigned char c : sofar) chars += (c & 0xC0) != 0x80; if ((int)chars >= f.max_length) break; } if (!ended) s += log_prob(logits, no_refs, term[0]); js << "{\"value\":" << json_str(detokenize(m, gen)) << ",\"probability\":" << json_num(std::exp(s)) << "}"; answer = term; // the generated tokens are already in the context } else { if (refs.empty()) { js << "{\"value\":null,\"probability\":null}"; answer = encode_text(m, "-"); answer.insert(answer.end(), term.begin(), term.end()); } else { float mx = -1e30f; for (int r : refs) mx = std::max(mx, logits[r]); double den = 0.0; size_t best = 0; for (size_t k = 0; k < refs.size(); k++) { den += std::exp((double)logits[refs[k]] - mx); if (logits[refs[k]] > logits[refs[best]]) best = k; } const std::vector& b = ref_boxes[best]; js << "{\"value\":[" << b[0] << "," << b[1] << "," << b[2] << "," << b[3] << "],\"probability\":" << json_num(std::exp((double)logits[refs[best]] - mx) / den) << "}"; answer = {refs[best]}; answer.insert(answer.end(), term.begin(), term.end()); } } if (!last) for (int t : answer) logits = dec.step(t); } js << "}"; return js.str(); } std::string detokenize(const Model& m, const std::vector& ids) { std::string out, bytes; auto flush = [&]() { out += bytes; bytes.clear(); }; for (int id : ids) { if (id >= m.n_pieces) { flush(); int k = id - m.ref_base; out += k < 64 ? "" : ""; continue; } const std::string& p = m.pieces[id]; int t = m.piece_types[id]; if (t == 4 && p.size() == 6 && p.rfind("<0x", 0) == 0) { bytes += (char)std::stoi(p.substr(3, 2), nullptr, 16); continue; } flush(); if (t == 0) { std::string s = p; size_t pos; while ((pos = s.find("\xE2\x96\x81")) != std::string::npos) s.replace(pos, 3, " "); out += s; } else { out += p; } } flush(); return out; } } // namespace oasis // ==== Record assembly, mask outlines and bitmaps, forms, options parsing and the C API namespace oasis { // The calling thread's OpenMP thread count set to engine_threads() for one call, and restored after it. struct ThreadScope { #ifdef _OPENMP int prev = omp_get_max_threads(); ThreadScope() { omp_set_num_threads(engine_threads()); } ~ThreadScope() { omp_set_num_threads(prev); } #endif }; // ------------------------------------------------------------------ JSON writing static std::string esc(const std::string& s) { std::string o = "\""; for (unsigned char c : s) { if (c == '"') o += "\\\""; else if (c == '\\') o += "\\\\"; else if (c == '\n') o += "\\n"; else if (c == '\t') o += "\\t"; else if (c < 0x20) { char b[8]; std::snprintf(b, sizeof b, "\\u%04x", c); o += b; } else o += (char)c; } return o + "\""; } static std::string num(double v, int prec = 3) { if (!std::isfinite(v)) return "null"; char b[32]; std::snprintf(b, sizeof b, "%.*f", prec, v); std::string s = b; if (s.find('.') != std::string::npos) { while (s.back() == '0') s.pop_back(); if (s.back() == '.') s.pop_back(); } return s; } static std::string trim(const std::string& s) { size_t a = s.find_first_not_of(" \t\r"), b = s.find_last_not_of(" \t\r"); return a == std::string::npos ? "" : s.substr(a, b - a + 1); } // ------------------------------------------------------------------ JSON reading struct JV { enum Type { NUL, BOOL, NUM, STR, ARR, OBJ } t = NUL; bool b = false; double n = 0.0; std::string s; // string value, or a number's source text std::vector a; std::vector> o; // members in document order const JV* get(const char* k) const { for (const auto& kv : o) if (kv.first == k) return &kv.second; return nullptr; } }; struct JReader { const char* p; const char* e; [[noreturn]] void fail(const std::string& what) { throw std::runtime_error("invalid options: " + what); } void ws() { while (p < e && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++; } static void utf8(std::string& out, unsigned cp) { if (cp < 0x80) out += (char)cp; else if (cp < 0x800) { out += (char)(0xC0 | (cp >> 6)); out += (char)(0x80 | (cp & 0x3F)); } else if (cp < 0x10000) { out += (char)(0xE0 | (cp >> 12)); out += (char)(0x80 | ((cp >> 6) & 0x3F)); out += (char)(0x80 | (cp & 0x3F)); } else { out += (char)(0xF0 | (cp >> 18)); out += (char)(0x80 | ((cp >> 12) & 0x3F)); out += (char)(0x80 | ((cp >> 6) & 0x3F)); out += (char)(0x80 | (cp & 0x3F)); } } unsigned hex4() { if (e - p < 4) fail("bad \\u escape"); unsigned v = 0; for (int i = 0; i < 4; i++) { char c = *p++; v <<= 4; if (c >= '0' && c <= '9') v |= (unsigned)(c - '0'); else if (c >= 'a' && c <= 'f') v |= (unsigned)(c - 'a' + 10); else if (c >= 'A' && c <= 'F') v |= (unsigned)(c - 'A' + 10); else fail("bad \\u escape"); } return v; } std::string str() { p++; std::string out; while (p < e && *p != '"') { char c = *p++; if (c != '\\') { out += c; continue; } if (p >= e) fail("unterminated string"); char x = *p++; switch (x) { case '"': out += '"'; break; case '\\': out += '\\'; break; case '/': out += '/'; break; case 'b': out += '\b'; break; case 'f': out += '\f'; break; case 'n': out += '\n'; break; case 'r': out += '\r'; break; case 't': out += '\t'; break; case 'u': { unsigned cp = hex4(); if (cp >= 0xD800 && cp < 0xDC00 && e - p >= 6 && p[0] == '\\' && p[1] == 'u') { p += 2; unsigned lo = hex4(); cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); } utf8(out, cp); break; } default: fail("bad escape in string"); } } if (p >= e) fail("unterminated string"); p++; return out; } JV value() { ws(); if (p >= e) fail("unexpected end of JSON"); JV v; if (*p == '{') { p++; v.t = JV::OBJ; ws(); if (p < e && *p == '}') { p++; return v; } for (;;) { ws(); if (p >= e || *p != '"') fail("expected a key"); std::string k = str(); ws(); if (p >= e || *p != ':') fail("expected ':'"); p++; JV x = value(); v.o.emplace_back(std::move(k), std::move(x)); ws(); if (p < e && *p == ',') { p++; continue; } if (p < e && *p == '}') { p++; return v; } fail("expected ',' or '}'"); } } if (*p == '[') { p++; v.t = JV::ARR; ws(); if (p < e && *p == ']') { p++; return v; } for (;;) { v.a.push_back(value()); ws(); if (p < e && *p == ',') { p++; continue; } if (p < e && *p == ']') { p++; return v; } fail("expected ',' or ']'"); } } if (*p == '"') { v.t = JV::STR; v.s = str(); return v; } auto word = [&](const char* w) { size_t n = std::strlen(w); if ((size_t)(e - p) >= n && std::strncmp(p, w, n) == 0) { p += n; return true; } return false; }; if (word("true")) { v.t = JV::BOOL; v.b = true; return v; } if (word("false")) { v.t = JV::BOOL; return v; } if (word("null")) return v; const char* q = p; while (p < e && (std::isdigit((unsigned char)*p) || *p == '-' || *p == '+' || *p == '.' || *p == 'e' || *p == 'E')) p++; if (q == p) fail("unexpected character"); v.t = JV::NUM; v.s.assign(q, p); char* end = nullptr; v.n = std::strtod(v.s.c_str(), &end); if (!end || *end) fail("bad number " + v.s); return v; } }; // ------------------------------------------------------------------ mask geometry using Ring = std::vector>; // vertices in mask-grid units, cell (i, j) centred at (j + 0.5, i + 0.5) static double ring_area(const Ring& r) { double a = 0.0; for (size_t i = 0, n = r.size(); i < n; i++) { const auto& p = r[i]; const auto& q = r[(i + 1) % n]; a += (double)p.first * q.second - (double)q.first * p.second; } return 0.5 * a; } // Douglas-Peucker on a closed ring: split at the vertex farthest from the first, simplify both halves. static Ring simplify_ring(const Ring& pts, float tol) { const size_t n = pts.size(); if (n < 4) return pts; size_t far = 0; float fd = -1.0f; for (size_t i = 1; i < n; i++) { float d = std::hypot(pts[i].first - pts[0].first, pts[i].second - pts[0].second); if (d > fd) { fd = d; far = i; } } std::vector keep(n, 0); keep[0] = keep[far] = 1; auto pt = [&](size_t i) { return pts[i % n]; }; std::vector> st{{0, far}, {far, n}}; while (!st.empty()) { auto [a, c] = st.back(); st.pop_back(); auto A = pt(a), C = pt(c); float L = std::hypot(C.first - A.first, C.second - A.second), best = 0.0f; size_t bi = 0; for (size_t i = a + 1; i < c; i++) { auto P = pt(i); float d = L > 0 ? std::fabs((C.first - A.first) * (A.second - P.second) - (A.first - P.first) * (C.second - A.second)) / L : std::hypot(P.first - A.first, P.second - A.second); if (d > best) { best = d; bi = i; } } if (bi && best > tol) { keep[bi % n] = 1; st.push_back({a, bi}); st.push_back({bi, c}); } } Ring out; for (size_t i = 0; i < n; i++) if (keep[i]) out.push_back(pts[i]); return out; } // Exterior outlines of the thresholded 128 x 128 probability field, largest first. Marching squares over // the cell centres, with every crossing interpolated linearly between the two neighbouring probabilities; // the field is zero outside the grid, so every outline closes. Hole boundaries are left out. static std::vector outlines(const std::vector& prob, float threshold, bool largest_only) { const int N = 128, M = N + 2; // lattice -1 .. N, stored with an offset of one const float t = std::max(threshold, 1e-6f); auto f = [&](int i, int j) -> float { if (i < 0 || j < 0 || i >= N || j >= N) return -t; return prob[(size_t)i * N + j] - t; }; auto inside = [&](int i, int j) { return f(i, j) >= 0.0f; }; // Edge ids: horizontal edge from lattice (i, j) to (i, j + 1), vertical edge from (i, j) to (i + 1, j). auto hid = [&](int i, int j) { return (((i + 1) * M) + (j + 1)) * 2; }; auto vid = [&](int i, int j) { return (((i + 1) * M) + (j + 1)) * 2 + 1; }; auto cross = [&](int i0, int j0, int i1, int j1) { float a = f(i0, j0), b = f(i1, j1); float s = a / (a - b); return std::make_pair(j0 + 0.5f + s * (j1 - j0), i0 + 0.5f + s * (i1 - i0)); }; enum { T, R, B, L }; struct Seg { int from, to; std::pair p; }; std::vector segs; std::vector start_at((size_t)M * M * 2, -1); for (int i = -1; i < N; i++) for (int j = -1; j < N; j++) { int code = (inside(i, j) ? 8 : 0) | (inside(i, j + 1) ? 4 : 0) | (inside(i + 1, j + 1) ? 2 : 0) | (inside(i + 1, j) ? 1 : 0); if (code == 0 || code == 15) continue; const int eid[4] = {hid(i, j), vid(i, j + 1), hid(i + 1, j), vid(i, j)}; auto point = [&](int e) { switch (e) { case T: return cross(i, j, i, j + 1); case R: return cross(i, j + 1, i + 1, j + 1); case B: return cross(i + 1, j, i + 1, j + 1); default: return cross(i, j, i + 1, j); } }; // Directed so that the inside lies to the right of travel on the image grid (y down). int pairs[2][2], np = 0; auto add = [&](int a, int b) { pairs[np][0] = a; pairs[np][1] = b; np++; }; float centre = 0.25f * (f(i, j) + f(i, j + 1) + f(i + 1, j + 1) + f(i + 1, j)); switch (code) { case 1: add(L, B); break; case 2: add(B, R); break; case 3: add(L, R); break; case 4: add(R, T); break; case 6: add(B, T); break; case 7: add(L, T); break; case 8: add(T, L); break; case 9: add(T, B); break; case 11: add(T, R); break; case 12: add(R, L); break; case 13: add(R, B); break; case 14: add(B, L); break; case 5: if (centre >= 0.0f) { add(L, T); add(R, B); } else { add(R, T); add(L, B); } break; case 10: if (centre >= 0.0f) { add(T, R); add(B, L); } else { add(T, L); add(B, R); } break; } for (int k = 0; k < np; k++) { start_at[eid[pairs[k][0]]] = (int)segs.size(); segs.push_back({eid[pairs[k][0]], eid[pairs[k][1]], point(pairs[k][0])}); } } std::vector used(segs.size(), 0); std::vector rings; for (size_t s0 = 0; s0 < segs.size(); s0++) { if (used[s0]) continue; Ring r; int s = (int)s0; while (s >= 0 && !used[s]) { used[s] = 1; r.push_back(segs[s].p); s = start_at[segs[s].to]; } if (r.size() >= 3) rings.push_back(std::move(r)); } if (rings.empty()) return rings; // Outer boundaries share one orientation and holes the other; the largest ring is always an outer one. size_t big = 0; for (size_t k = 1; k < rings.size(); k++) if (std::fabs(ring_area(rings[k])) > std::fabs(ring_area(rings[big]))) big = k; const bool positive = ring_area(rings[big]) > 0; std::vector> outer; for (auto& r : rings) { double a = ring_area(r); if ((a > 0) == positive && std::fabs(a) >= 0.25) outer.push_back({std::fabs(a), simplify_ring(r, 0.5f)}); } std::stable_sort(outer.begin(), outer.end(), [](const auto& a, const auto& b) { return a.first > b.first; }); std::vector out; for (auto& o : outer) { if (o.second.size() < 3) continue; out.push_back(std::move(o.second)); if (largest_only) break; } return out; } // All pieces joined into one path: each piece is entered from the nearest vertex of the path so far, // walked once round, and left by the same bridge. static Ring merged_ring(const std::vector& rings) { if (rings.empty()) return {}; Ring path = rings[0]; for (size_t k = 1; k < rings.size(); k++) { const Ring& r = rings[k]; size_t bi = 0, bj = 0; float bd = 1e30f; for (size_t i = 0; i < path.size(); i++) for (size_t j = 0; j < r.size(); j++) { float dx = path[i].first - r[j].first, dy = path[i].second - r[j].second, d = dx * dx + dy * dy; if (d < bd) { bd = d; bi = i; bj = j; } } Ring out(path.begin(), path.begin() + (long)bi + 1); for (size_t t = 0; t <= r.size(); t++) out.push_back(r[(bj + t) % r.size()]); out.insert(out.end(), path.begin() + (long)bi, path.end()); path.swap(out); } return path; } // Column-major run-length encoding at the original image size, starting with a background run: the // probability field is sampled bilinearly at every pixel centre, thresholded and clipped to the box. static std::string bitmap_rle(const std::vector& prob, const int bb[4], const Meta& mt, float threshold) { const int W = mt.W, H = mt.H; const double bx0 = bb[0] / 1000.0 * W, by0 = bb[1] / 1000.0 * H, bx1 = bb[2] / 1000.0 * W, by1 = bb[3] / 1000.0 * H; const int x0 = std::max(0, (int)std::floor(bx0)), x1 = std::min(W, (int)std::ceil(bx1)); const int y0 = std::max(0, (int)std::floor(by0)), y1 = std::min(H, (int)std::ceil(by1)); std::vector counts; long long run = 0; bool fg = false; auto push = [&](bool v, long long n) { if (n <= 0) return; if (v == fg) { run += n; return; } counts.push_back(run); run = n; fg = v; }; const double k = mt.r / 4.0; // original pixels -> mask cells for (int x = 0; x < W; x++) { if (x < x0 || x >= x1 || y1 <= y0) { push(false, H); continue; } push(false, y0); double u = (x + 0.5) * k - 0.5; int j0 = (int)std::floor(u); double fu = u - j0; int ja = std::clamp(j0, 0, 127), jb = std::clamp(j0 + 1, 0, 127); bool colin = x + 0.5 >= bx0 && x + 0.5 <= bx1; for (int y = y0; y < y1; y++) { bool v = false; if (colin && y + 0.5 >= by0 && y + 0.5 <= by1) { double w = (y + 0.5) * k - 0.5; int i0 = (int)std::floor(w); double fv = w - i0; int ia = std::clamp(i0, 0, 127), ib = std::clamp(i0 + 1, 0, 127); double p = (1 - fv) * ((1 - fu) * prob[ia * 128 + ja] + fu * prob[ia * 128 + jb]) + fv * ((1 - fu) * prob[ib * 128 + ja] + fu * prob[ib * 128 + jb]); v = p >= threshold; } push(v, 1); } push(false, H - y1); } counts.push_back(run); std::ostringstream os; os << "{\"size\":[" << H << "," << W << "],\"counts\":["; for (size_t i = 0; i < counts.size(); i++) os << (i ? "," : "") << counts[i]; os << "]}"; return os.str(); } // ------------------------------------------------------------------ describe struct Entity { int n = 0; std::string name, position, count; std::vector> attrs; std::vector> refs; }; static std::vector> parse_attrs(const std::string& s) { std::vector> out; std::stringstream ss(s); std::string part; while (std::getline(ss, part, ';')) { auto eq = part.find('='); if (eq == std::string::npos) continue; out.push_back({trim(part.substr(0, eq)), trim(part.substr(eq + 1))}); } return out; } static bool has(const std::vector& v, const char* x) { return std::find(v.begin(), v.end(), x) != v.end(); } std::string describe_json(Model& m, const uint8_t* rgb, int W, int H, const Options& o, double* tm) { ThreadScope threads; using clk = std::chrono::steady_clock; auto ms = [](clk::time_point x, clk::time_point y) { return std::chrono::duration(y - x).count(); }; auto t0 = clk::now(); Encoded enc = encode(m, rgb, W, H); auto t1 = clk::now(); std::vector objects, texts; detect(enc, o.conf, o.text_conf, o.nms, o.max_objects, o.max_text_lines, objects, texts); const Meta& mt = enc.meta; auto to1000 = [&](const Box& b, int out[4]) { double sx = 512.0 / mt.r / mt.W * 1000.0, sy = 512.0 / mt.r / mt.H * 1000.0; out[0] = (int)std::nearbyint(b.x0 * sx); out[1] = (int)std::nearbyint(b.y0 * sy); out[2] = (int)std::nearbyint(b.x1 * sx); out[3] = (int)std::nearbyint(b.y1 * sy); }; if (!o.form.empty()) { std::vector M; std::vector mv; std::vector refs; memory(m, enc, objects, texts, M, mv, refs); std::vector> ref_boxes; for (const auto* list : {&objects, &texts}) for (const Box& b : *list) { int bb[4]; to1000(b, bb); ref_boxes.push_back({bb[0], bb[1], bb[2], bb[3]}); } auto t2 = clk::now(); std::string form = fill_form(m, M, mv, refs, ref_boxes, o.language, o.form); auto t3 = clk::now(); if (tm) { tm[0] = ms(t0, t1); tm[1] = ms(t1, t2); tm[2] = ms(t2, t3); tm[3] = ms(t0, t3); tm[4] = 0; } return "{\"form\":" + form + ",\"timing_ms\":{\"encode\":" + num(ms(t0, t1), 1) + ",\"detect\":" + num(ms(t1, t2), 1) + ",\"form\":" + num(ms(t2, t3), 1) + ",\"total\":" + num(ms(t0, t3), 1) + "}}"; } auto wants = [&](const char* s) { return o.sections.empty() || has(o.sections, s); }; auto field = [&](const char* f) { return o.fields.empty() || has(o.fields, f); }; std::vector ocr; for (auto& b : texts) { if (!wants("text")) { ocr.push_back(""); continue; } int bb[4]; to1000(b, bb); ocr.push_back(read_text(m, rgb, W, H, bb[0] / 1000.0f * W, bb[1] / 1000.0f * H, bb[2] / 1000.0f * W, bb[3] / 1000.0f * H)); } auto t2 = clk::now(); bool need_decoder = wants("scene") || wants("objects") || wants("regions") || wants("relations") || wants("events") || wants("reasoning"); std::vector M; std::vector mv; std::vector refs; std::vector ids; if (need_decoder) { memory(m, enc, objects, texts, M, mv, refs); ids = generate(m, M, mv, refs, o.language, o.max_tokens, o.keywords); } auto t3 = clk::now(); std::string text = detokenize(m, ids); std::string trace, body = text; size_t a = text.find(""), b = text.find(""); if (a != std::string::npos && b != std::string::npos && b > a) { trace = trim(text.substr(a + 7, b - a - 7)); body = text.substr(b + 8); } std::vector> scene; std::vector ents; std::vector text_refs; std::vector rel_json, events; std::stringstream ls(body); std::string line; while (std::getline(ls, line)) { line = trim(line); if (line.empty()) continue; if (line.rfind("S ", 0) == 0) { for (auto& kv : parse_attrs(line.substr(2))) scene.push_back(kv); } else if (line.size() > 1 && line[0] == 'E' && std::isdigit((unsigned char)line[1])) { Entity e; size_t sp = line.find(' '); e.n = std::atoi(line.substr(1, sp).c_str()); std::string rest = sp == std::string::npos ? "" : line.substr(sp + 1); size_t br = rest.rfind('{'); if (br != std::string::npos && !rest.empty() && rest.back() == '}') { e.attrs = parse_attrs(rest.substr(br + 1, rest.size() - br - 2)); rest = trim(rest.substr(0, br)); } size_t r0; while ((r0 = rest.find('<')) != std::string::npos) { size_t r1 = rest.find('>', r0); if (r1 == std::string::npos) break; std::string tag = rest.substr(r0 + 1, r1 - r0 - 1); if (tag.rfind("obj", 0) == 0) e.refs.push_back({'o', std::atoi(tag.c_str() + 3)}); else if (tag.rfind("txt", 0) == 0) e.refs.push_back({'t', std::atoi(tag.c_str() + 3)}); rest.erase(r0, r1 - r0 + 1); } for (auto& c : rest) if (c == '*') c = ' '; size_t at = rest.find('@'); if (at != std::string::npos) { e.position = trim(rest.substr(at + 1)); rest = rest.substr(0, at); } rest = trim(rest); size_t hs = rest.rfind(" #"); if (hs != std::string::npos) { e.count = trim(rest.substr(hs + 2)); rest = trim(rest.substr(0, hs)); } e.name = rest; ents.push_back(e); } else if (line[0] == 'T') { size_t r0 = 0; while ((r0 = line.find("> s1 >> pred >> s2) rel_json.push_back("{\"subject\":" + std::to_string(s1) + ",\"predicate\":" + esc(pred) + ",\"object\":" + std::to_string(s2) + "}"); } else if (line.rfind("V ", 0) == 0) { events.push_back(trim(line.substr(2))); } } std::vector protos; const bool draw = o.masks && o.mask_format != "none" && field("mask"); auto mask_json = [&](const Box& box, const int bb[4]) -> std::string { std::vector prob = mask_prob(m, enc, box, &protos); if (o.mask_format == "bitmap") return bitmap_rle(prob, bb, mt, o.mask_threshold); double sx = 512.0 / 128.0 / mt.r / mt.W * 1000.0, sy = 512.0 / 128.0 / mt.r / mt.H * 1000.0; auto ring_str = [&](const Ring& rg) { std::ostringstream rs; rs << "["; for (size_t i = 0; i < rg.size(); i++) rs << (i ? "," : "") << (int)std::nearbyint(std::clamp(rg[i].first * sx, 0.0, 1000.0)) << "," << (int)std::nearbyint(std::clamp(rg[i].second * sy, 0.0, 1000.0)); rs << "]"; return rs.str(); }; std::vector rings = outlines(prob, o.mask_threshold, o.mask_format == "largest"); if (o.mask_format == "largest") return rings.empty() ? "[]" : ring_str(rings[0]); if (o.mask_format == "merged") return ring_str(merged_ring(rings)); std::string s = "["; for (size_t i = 0; i < rings.size(); i++) s += (i ? "," : "") + ring_str(rings[i]); return s + "]"; }; auto common = [&](const Entity& e) { std::ostringstream it; it << "{\"id\":" << e.n << ",\"name\":" << esc(e.name); if (!e.count.empty() && field("count")) { bool digits = std::all_of(e.count.begin(), e.count.end(), ::isdigit); it << ",\"count\":" << (digits ? e.count : esc(e.count)); } if (!e.position.empty() && field("position")) it << ",\"position\":" << esc(e.position); if (!e.attrs.empty() && field("description")) { it << ",\"description\":{"; for (size_t i = 0; i < e.attrs.size(); i++) it << (i ? "," : "") << esc(e.attrs[i].first) << ":" << esc(e.attrs[i].second); it << "}"; } return it.str(); }; // One entry per referenced instance, sharing the entity's id, name and description. std::ostringstream objs_js, regs_js; bool ofirst = true, rfirst = true; for (auto& e : ents) { std::vector inst; for (auto& r : e.refs) if (r.first == 'o' && r.second >= 0 && r.second < (int)objects.size()) inst.push_back(r.second); if (inst.empty()) { regs_js << (rfirst ? "" : ",") << common(e) << "}"; rfirst = false; continue; } for (int k : inst) { int bb[4]; to1000(objects[k], bb); objs_js << (ofirst ? "" : ",") << common(e); if (field("bbox")) objs_js << ",\"bbox\":[" << bb[0] << "," << bb[1] << "," << bb[2] << "," << bb[3] << "]"; if (field("score")) objs_js << ",\"score\":" << num(objects[k].score); if (draw) objs_js << ",\"mask\":" << mask_json(objects[k], bb); objs_js << "}"; ofirst = false; } } std::ostringstream js; std::vector parts; if (wants("scene")) { std::string s = "\"scene\":{"; for (size_t i = 0; i < scene.size(); i++) s += (i ? "," : "") + esc(scene[i].first) + ":" + esc(scene[i].second); parts.push_back(s + "}"); } if (wants("objects")) parts.push_back("\"objects\":[" + objs_js.str() + "]"); if (wants("regions")) parts.push_back("\"regions\":[" + regs_js.str() + "]"); if (wants("text")) { std::ostringstream ts; bool first = true; for (size_t k = 0; k < texts.size(); k++) { if (ocr[k].empty()) continue; int bb[4]; to1000(texts[k], bb); bool mentioned = std::find(text_refs.begin(), text_refs.end(), (int)k) != text_refs.end(); ts << (first ? "" : ",") << "{\"string\":" << esc(ocr[k]); if (field("bbox")) ts << ",\"bbox\":[" << bb[0] << "," << bb[1] << "," << bb[2] << "," << bb[3] << "]"; if (field("score")) ts << ",\"score\":" << num(texts[k].score); if (need_decoder) ts << ",\"mentioned\":" << (mentioned ? "true" : "false"); ts << "}"; first = false; } parts.push_back("\"text\":[" + ts.str() + "]"); } if (wants("relations")) { std::string s = "\"relations\":["; for (size_t i = 0; i < rel_json.size(); i++) s += (i ? "," : "") + rel_json[i]; parts.push_back(s + "]"); } if (wants("events")) { std::string s = "\"events\":["; for (size_t i = 0; i < events.size(); i++) s += (i ? "," : "") + esc(events[i]); parts.push_back(s + "]"); } if (wants("reasoning") && o.reasoning) parts.push_back("\"reasoning\":" + esc(trace)); auto t4 = clk::now(); parts.push_back("\"timing_ms\":{\"encode\":" + num(ms(t0, t1), 1) + ",\"detect_read\":" + num(ms(t1, t2), 1) + ",\"decode\":" + num(ms(t2, t3), 1) + ",\"assemble\":" + num(ms(t3, t4), 1) + ",\"total\":" + num(ms(t0, t4), 1) + ",\"tokens\":" + std::to_string(ids.size()) + "}"); js << "{"; for (size_t i = 0; i < parts.size(); i++) js << (i ? "," : "") << parts[i]; js << "}"; if (tm) { tm[0] = ms(t0, t1); tm[1] = ms(t1, t2); tm[2] = ms(t2, t3); tm[3] = ms(t0, t4); tm[4] = (double)ids.size(); } return js.str(); } // ------------------------------------------------------------------ options static void parse_form(const JV& v, Options& o) { auto bad = [](const std::string& what) { throw std::runtime_error("invalid form: " + what); }; if (v.t != JV::OBJ) bad("form must be an object of fields"); if (v.o.empty()) bad("form has no fields"); for (const auto& kv : v.o) { const std::string& name = kv.first; const JV& s = kv.second; if (name.empty() || name.find_first_of("=;|#$@\n ") != std::string::npos) bad("field name " + esc(name) + " cannot be used"); if (s.t != JV::OBJ) bad("field " + name + " must be an object"); FormField f; f.name = name; const JV* en = s.get("enum"); const JV* xs = s.get("x-source"); const JV* ty = s.get("type"); bool done = false; if (en) { if (en->t != JV::ARR || en->a.empty()) bad("field " + name + " needs an enum list"); for (const JV& c : en->a) { if (c.t == JV::STR) { f.choices.push_back(c.s); f.values.push_back(esc(c.s)); } else if (c.t == JV::NUM) { f.choices.push_back(c.s); f.values.push_back(c.s); } else bad("field " + name + " has a choice that is not a string or a number"); if (f.choices.back().empty() || f.choices.back().find_first_of("|\n") != std::string::npos) bad("field " + name + " has a choice that cannot be used"); } f.kind = FormField::CHOICE; done = true; } else if (xs) { if (xs->t != JV::STR) bad("field " + name + " needs a string x-source"); if (xs->s == "bbox") { const JV* of = s.get("x-of"); bool ok = false; if (of && of->t == JV::STR) for (const FormField& g : o.form) ok = ok || (g.name == of->s && g.kind == FormField::CHOICE); if (!ok) bad("box field " + name + " must point at an earlier choice field"); f.kind = FormField::BOX; f.of = of->s; done = true; } } if (!done && ty && ty->t == JV::STR && ty->s == "integer") { const JV* lo = s.get("minimum"); const JV* hi = s.get("maximum"); if (!lo || !hi || lo->t != JV::NUM || hi->t != JV::NUM || lo->n != std::floor(lo->n) || hi->n != std::floor(hi->n) || lo->n > hi->n) bad("field " + name + " needs an integer minimum and maximum"); if (hi->n - lo->n > 1000) bad("field " + name + " spans more than 1000 values"); f.kind = FormField::COUNT; f.lo = (long)lo->n; f.hi = (long)hi->n; done = true; } if (!done && ty && ty->t == JV::STR && ty->s == "string") { const JV* ml = s.get("maxLength"); if (ml) { if (ml->t != JV::NUM || ml->n < 1 || ml->n > 500) bad("field " + name + " needs a maxLength from 1 to 500"); f.max_length = (int)ml->n; } f.kind = FormField::TEXT; done = true; } if (!done) bad("field " + name + " is not a choice, count, box or text field"); o.form.push_back(f); } } static void parse_options(const Model& m, const char* s, Options& o) { if (!s || !*s) return; JReader jr{s, s + std::strlen(s)}; JV root = jr.value(); jr.ws(); if (jr.p != jr.e) jr.fail("trailing characters after the JSON object"); if (root.t != JV::OBJ) jr.fail("options must be a JSON object"); static const char* SECTIONS[] = {"scene", "objects", "regions", "text", "relations", "events", "reasoning"}; static const char* FIELDS[] = {"bbox", "score", "mask", "position", "description", "count"}; for (const auto& kv : root.o) { const std::string& k = kv.first; const JV& v = kv.second; auto number = [&](double lo, double hi) { if (v.t != JV::NUM || !(v.n >= lo && v.n <= hi)) jr.fail(k + " must be a number from " + num(lo, 2) + " to " + num(hi, 2)); return v.n; }; auto boolean = [&]() { if (v.t != JV::BOOL) jr.fail(k + " must be true or false"); return v.b; }; auto names = [&](const char* const* allowed, size_t n, std::vector& out) { if (v.t != JV::ARR) jr.fail(k + " must be a list of names"); for (const JV& x : v.a) { bool ok = x.t == JV::STR && std::find(allowed, allowed + n, x.s) != allowed + n; if (!ok) jr.fail(k + " has an unknown name " + (x.t == JV::STR ? x.s : std::string("(not a string)"))); out.push_back(x.s); } }; if (k == "language") { if (v.t != JV::STR || std::find(m.languages.begin(), m.languages.end(), v.s) == m.languages.end()) { std::string list; for (const auto& l : m.languages) list += (list.empty() ? "" : ", ") + l; jr.fail("unsupported language " + (v.t == JV::STR ? v.s : std::string("(not a string)")) + "; choose one of " + list); } o.language = v.s; } else if (k == "keywords") { std::vector ks; if (v.t == JV::STR) { std::stringstream ss(v.s); std::string line; while (std::getline(ss, line)) ks.push_back(line); } else if (v.t == JV::ARR) { for (const JV& x : v.a) { if (x.t != JV::STR) jr.fail("keywords must be strings"); ks.push_back(x.s); } } else { jr.fail("keywords must be a list of strings or one newline-separated string"); } for (auto& s : ks) { s = trim(s); if (s.empty()) continue; if (s.size() > 100) jr.fail("a keyword is longer than 100 bytes"); o.keywords.push_back(s); } if (o.keywords.size() > 64) jr.fail("more than 64 keywords"); } else if (k == "conf") o.conf = (float)number(0, 1); else if (k == "text_conf") o.text_conf = (float)number(0, 1); else if (k == "mask_threshold") o.mask_threshold = (float)number(0, 1); else if (k == "max_objects") o.max_objects = (int)number(0, 64); else if (k == "max_text_lines") o.max_text_lines = (int)number(0, 32); else if (k == "max_tokens") o.max_tokens = (int)number(1, 1536); else if (k == "masks") o.masks = boolean(); else if (k == "nms") o.nms = boolean(); else if (k == "reasoning") o.reasoning = boolean(); else if (k == "mask_format") { if (v.t != JV::STR || (v.s != "largest" && v.s != "polygons" && v.s != "merged" && v.s != "bitmap" && v.s != "none")) jr.fail("mask_format must be largest, polygons, merged, bitmap or none"); o.mask_format = v.s; } else if (k == "sections") names(SECTIONS, 7, o.sections); else if (k == "fields") names(FIELDS, 6, o.fields); else if (k == "form") parse_form(v, o); } } } // namespace oasis // ------------------------------------------------------------------ C API static thread_local std::string g_error; struct oasis_model { oasis::Model m; }; extern "C" { oasis_model* oasis_load(const char* path) { const char* ex = std::getenv("OASIS_EXACT"); oasis::g_exact_gemv = ex && ex[0] == '1'; auto* h = new oasis_model(); std::string err; try { if (!h->m.load(path, err)) { g_error = err; delete h; return nullptr; } } catch (const std::exception& e) { g_error = e.what(); delete h; return nullptr; } return h; } void oasis_free(oasis_model* h) { delete h; } int oasis_describe(oasis_model* h, const unsigned char* rgb, int width, int height, const char* options_json, char* out, int capacity) { if (!h || !rgb || width <= 0 || height <= 0) { g_error = "invalid arguments"; return -1; } try { oasis::Options o; oasis::parse_options(h->m, options_json, o); std::string js = oasis::describe_json(h->m, rgb, width, height, o, nullptr); if ((int)js.size() + 1 > capacity) { g_error = "output buffer too small"; return -(int)js.size() - 1; } std::memcpy(out, js.c_str(), js.size() + 1); return (int)js.size(); } catch (const std::exception& e) { g_error = e.what(); return -1; } } const char* oasis_last_error(void) { return g_error.c_str(); } } // extern "C"