diff --git a/CMakeLists.txt b/CMakeLists.txt index 354f1d9..972d242 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,19 @@ cmake_minimum_required(VERSION 3.16) project(soft-cuda LANGUAGES CXX CUDA) +# Core library sources — explicitly exclude the Python bridge subdir file(GLOB_RECURSE LIB_SOURCE src/*.cu src/*.cpp src/*.hpp src/*.h) +list(FILTER LIB_SOURCE EXCLUDE REGEX ".*/python/.*") set(CMAKE_CUDA_ARCHITECTURES native) add_library(soft_lib SHARED ${LIB_SOURCE}) +set_target_properties(soft_lib PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) target_include_directories(soft_lib PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/backend_cpu/include" @@ -23,6 +30,8 @@ target_compile_options(soft_lib PRIVATE set_target_properties(soft_lib PROPERTIES CUDA_SEPARABLE_COMPILATION ON + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON ) # # target_compile_options(soft_lib PUBLIC @@ -33,9 +42,34 @@ target_compile_options(soft_lib PRIVATE $<$:-g>) # target_link_options(soft_lib PUBLIC # $<$:-fsanitize=address> # ) + + +# ───────────────────────────────────────────────────────────────────────────── +# Python C bridge — flat-C shared library for use with ctypes / cffi +# ───────────────────────────────────────────────────────────────────────────── +add_library(soft_cuda_python SHARED src/python/sc_bridge.cpp) + +set_target_properties(soft_cuda_python PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) + +target_include_directories(soft_cuda_python + PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/backend_cpu/include" + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/core/include" + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src" +) + add_executable(soft main.cpp) +target_link_libraries(soft_cuda_python PRIVATE soft_lib) +target_link_libraries(soft PRIVATE soft_cuda_python) -target_link_libraries(soft PRIVATE soft_lib) +# Ensure all sc_* symbols are exported on Linux/macOS +if(NOT WIN32) + target_compile_options(soft_cuda_python PRIVATE -fvisibility=default) +endif() enable_testing() add_subdirectory(tests) diff --git a/docs/PYTHON_BRIDGE.md b/docs/PYTHON_BRIDGE.md new file mode 100644 index 0000000..974d950 --- /dev/null +++ b/docs/PYTHON_BRIDGE.md @@ -0,0 +1,559 @@ + + +# soft-cuda Python Bridge — API Reference + +> **`include/soft-cuda/python/soft_cuda_python.h`** — the only header you need. +> **`libsoft_cuda_python.so`** (or `.dll`) — the shared library to load from Python. + +The Python bridge wraps the internal C++ tensor library behind a flat-C +(`extern "C"`) interface. Every symbol uses the **`sc_`** prefix and +only C-safe primitives — no `std::vector`, no `enum class`, no default +arguments. This makes it directly callable from Python via `ctypes` or +`cffi`. + +--- + +## Table of Contents + +- [Architecture](#architecture) +- [Building](#building) +- [Headers at a Glance](#headers-at-a-glance) +- [API Reference](#api-reference) + - [Pool Management — `tensor_pool.h`](#pool-management) + - [Tensor Lifecycle — `tensor_core.h`](#tensor-lifecycle) + - [Forward Ops — `tensor_ops.h`](#forward-ops) + - [Graph & Training — `tensor_graph.h`](#graph--training) + - [Persistence — `tensor_io.h`](#persistence) +- [Python ctypes Example](#python-ctypes-example) +- [C/C++ Usage Example](#cc-usage-example) +- [Memory Model](#memory-model) +- [Design Decisions](#design-decisions) + +--- + +## Architecture + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ Python (ctypes.CDLL / cffi) │ +│ │ +│ import ctypes │ +│ lib = ctypes.CDLL("./libsoft_cuda_python.so") │ +│ pool = lib.sc_pool_create(4*1024*1024, 0) │ +│ │ +├────────────────────────── extern "C" ABI ──────────────────────────────┤ +│ │ +│ sc_bridge.cpp │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ sc_pool_create() → tensor_pool_create() │ │ +│ │ sc_tensor_create() → tensor_create() + enum cast │ │ +│ │ sc_tensor_mul() → tensor_mul() │ │ +│ │ sc_build_graph() → verifyIfDAG + assignBackendGraph + ... │ │ +│ │ sc_graph_step() → forward + gpu_transfer + grad + bwd + sgd│ │ +│ │ sc_graph_t → wraps std::vector │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ +├────────────────────────── C++ internals ───────────────────────────────┤ +│ │ +│ soft_lib (shared library) │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ tensor.h, pool.h, DAGbuild.cpp, assignBackend.cu, train.cpp ... │ │ +│ │ backend_cpu/*, backend_gpu/*, saveLoad.cpp │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Building + +```bash +# Configure (once) +cmake -B build -DCMAKE_BUILD_TYPE=Release + +# Build everything (main executable + python bridge) +cmake --build build + +# Build only the Python bridge shared library +cmake --build build --target soft_cuda_python +``` + +The output is: + +| Target | File | Description | +|---|---|---| +| `soft` | `build/soft` | Main executable (demo) | +| `soft_lib` | `build/libsoft_lib.so` | Core C++ tensor library | +| `soft_cuda_python` | `build/libsoft_cuda_python.so` | Python-compatible bridge | + +--- + +## Headers at a Glance + +``` +include/soft-cuda/python/ +├── soft_cuda_python.h ← master include (just #includes the five below) +│ +├── tensor_pool.h ← arena management +│ sc_pool_create, sc_pool_destroy, sc_pool_zero, +│ sc_pool_alloc, sc_pool_size, sc_pool_used +│ +├── tensor_core.h ← tensor lifecycle + dtype constants +│ SC_DTYPE_FLOAT32, SC_DTYPE_INT32, ... +│ sc_tensor_create, sc_tensor_id, sc_tensor_get_data, +│ sc_tensor_get_ndims, sc_tensor_get_dims, +│ sc_tensor_print_data, sc_tensor_fill_random_normal +│ +├── tensor_ops.h ← forward ops + evaluate +│ sc_tensor_mul, sc_tensor_mul_naive, +│ sc_tensor_add, sc_tensor_add_bias, sc_tensor_sub, +│ sc_tensor_relu, sc_tensor_mean, sc_tensor_mse_loss, +│ sc_tensor_square, sc_tensor_transpose, +│ sc_tensor_evaluate, sc_tensor_evaluate_gpu +│ +├── tensor_graph.h ← graph building + training +│ (Layer 1) sc_graph_create, sc_graph_destroy, +│ sc_verify_dag, sc_assign_backend, +│ sc_assign_grad_memory, sc_graph_forward, +│ sc_autograd_gpu_transfer, +│ sc_grad_initializer, sc_backward, +│ sc_sgd, sc_node_to_host +│ (Layer 2) sc_build_graph, sc_graph_step, +│ sc_graph_get_loss, sc_graph_size +│ +└── tensor_io.h ← persistence + sc_save_model, sc_load_model +``` + +--- + +## API Reference + +### Pool Management + +> Defined in `tensor_pool.h` + +```c +/* Opaque type — never dereference, just pass the pointer around. */ +typedef struct tensor_pool_instance sc_pool_t; + +/* Create a new arena. on_device=1 → GPU VRAM, on_device=0 → CPU RAM. */ +sc_pool_t *sc_pool_create(size_t capacity_bytes, int on_device); + +/* Completely free the arena and return memory to the OS. */ +void sc_pool_destroy(sc_pool_t *pool); + +/* Reset bump pointer to zero (invalidates all tensors, keeps memory). */ +void sc_pool_zero(sc_pool_t *pool); + +/* Raw allocation from the pool. Returns NULL if exhausted. */ +void *sc_pool_alloc(sc_pool_t *pool, size_t size, uint32_t *out_id); + +/* Query capacity and usage. */ +size_t sc_pool_size(sc_pool_t *pool); +size_t sc_pool_used(sc_pool_t *pool); +``` + +> [!TIP] +> Use **separate pools** for data, metadata, GPU memory, and gradients. +> This lets you `sc_pool_zero(pool_grad)` between epochs without +> affecting your weights. + +--- + +### Tensor Lifecycle + +> Defined in `tensor_core.h` + +#### Data-type Constants + +```c +#define SC_DTYPE_UINT32 0 +#define SC_DTYPE_INT32 1 +#define SC_DTYPE_UINT64 2 +#define SC_DTYPE_INT64 3 +#define SC_DTYPE_FLOAT32 4 /* ← the one you'll use 99% of the time */ +#define SC_DTYPE_FLOAT64 5 +``` + +#### Functions + +```c +typedef struct tensor_instance sc_tensor_t; + +/* Create a tensor. Pass NULL for elems to get zero-initialised data. + grad = 1 → autograd will track this tensor (for weights). */ +sc_tensor_t *sc_tensor_create(sc_pool_t *pool, int dtype, + uint32_t num_dims, uint32_t *dims, + void *elems, int grad); + +uint32_t sc_tensor_id(sc_tensor_t *t); +void *sc_tensor_get_data(sc_tensor_t *t); +uint8_t sc_tensor_get_ndims(sc_tensor_t *t); +uint32_t *sc_tensor_get_dims(sc_tensor_t *t); +void sc_tensor_print_data(sc_tensor_t *t); +int sc_tensor_fill_random_normal(sc_tensor_t *t, float mean, float std_dev); +``` + +> [!IMPORTANT] +> Set `grad = 0` for input data and targets. +> Set `grad = 1` for trainable parameters (weights, biases). + +--- + +### Forward Ops + +> Defined in `tensor_ops.h` + +All ops are **lazy** — they don't compute anything. They create a new +tensor node that records the operation and its operands. Actual +computation happens when the graph's `forward` is called. + +```c +/* Arithmetic */ +sc_tensor_t *sc_tensor_mul(sc_pool_t *pool, sc_tensor_t *x, sc_tensor_t *y); +sc_tensor_t *sc_tensor_mul_naive(sc_pool_t *pool, sc_tensor_t *x, sc_tensor_t *y); +sc_tensor_t *sc_tensor_add(sc_pool_t *pool, sc_tensor_t *x, sc_tensor_t *y); +sc_tensor_t *sc_tensor_add_bias(sc_pool_t *pool, sc_tensor_t *xw, sc_tensor_t *bias); +sc_tensor_t *sc_tensor_sub(sc_pool_t *pool, sc_tensor_t *a, sc_tensor_t *b); +sc_tensor_t *sc_tensor_square(sc_pool_t *pool, sc_tensor_t *x); +sc_tensor_t *sc_tensor_transpose(sc_pool_t *pool, sc_tensor_t *a); + +/* Activation */ +sc_tensor_t *sc_tensor_relu(sc_pool_t *pool, sc_tensor_t *a); + +/* Reduction */ +sc_tensor_t *sc_tensor_mean(sc_pool_t *pool, sc_tensor_t *a); + +/* Loss */ +sc_tensor_t *sc_tensor_mse_loss(sc_pool_t *pool, + sc_tensor_t *predictions, + sc_tensor_t *target); + +/* Single-node evaluate (for debugging — you don't need these normally) */ +int sc_tensor_evaluate(sc_pool_t *pool, sc_tensor_t *t); +int sc_tensor_evaluate_gpu(sc_pool_t *pool, sc_tensor_t *t, + float *d_a, float *d_b, float *d_res); +``` + +> [!NOTE] +> `sc_tensor_mul` internally transposes B and does a cache-optimised +> matmul. `sc_tensor_mul_naive` is the straightforward O(n³) version, +> safer for small matrices (e.g. XOR). + +--- + +### Graph & Training + +> Defined in `tensor_graph.h` + +#### Backend Constants + +```c +#define SC_BACKEND_GPU 0 +#define SC_BACKEND_CPU 1 +#define SC_BACKEND_HYBRID 2 /* auto-dispatch per CONFIG.soft */ +``` + +#### Opaque Graph Handle + +```c +typedef struct sc_graph sc_graph_t; +``` + +This hides `std::vector` from Python/C. + +#### Layer 1 — Low-level Primitives + +```c +sc_graph_t *sc_graph_create(void); +void sc_graph_destroy(sc_graph_t *g); + +int sc_verify_dag(sc_pool_t *meta_pool, sc_tensor_t *t, sc_graph_t *g); +void sc_assign_backend(sc_pool_t *pool_gpu, sc_graph_t *g, int mode); +void sc_assign_grad_memory(sc_pool_t *pool_grad_cpu, + sc_pool_t *pool_grad_gpu, + sc_graph_t *g); + +int sc_graph_forward(sc_pool_t *pool_cpu, sc_pool_t *pool_gpu, sc_graph_t *g); +void sc_autograd_gpu_transfer(sc_graph_t *g); +void sc_grad_initializer(sc_graph_t *g); +int sc_backward(sc_graph_t *g); +void sc_sgd(sc_graph_t *g, float learning_rate); +int sc_node_to_host(sc_graph_t *g, size_t node_idx); +``` + +#### Layer 2 — Convenience API + +```c +/* One-liner: verify DAG → assign backend → alloc grads. Returns ready graph. */ +sc_graph_t *sc_build_graph(sc_pool_t *meta_pool, + sc_pool_t *pool_gpu, + sc_pool_t *pool_grad_cpu, + sc_pool_t *pool_grad_gpu, + sc_tensor_t *loss, + int backend_mode); + +/* One-liner: forward → gpu_transfer → zero_grads → backward → sgd. */ +void sc_graph_step(sc_pool_t *pool_cpu, + sc_pool_t *pool_gpu, + sc_graph_t *g, + float learning_rate); + +/* Read the scalar loss value (auto-transfers from GPU if needed). */ +float sc_graph_get_loss(sc_graph_t *g); + +/* Number of nodes in the topological order. */ +size_t sc_graph_size(sc_graph_t *g); +``` + +> [!TIP] +> **For Python users**, the Layer 2 API is almost always what you want. +> Two function calls cover the entire training loop: +> ```python +> graph = lib.sc_build_graph(meta, gpu, gcpu, ggpu, loss, SC_BACKEND_CPU) +> for epoch in range(10000): +> lib.sc_graph_step(pool, gpu, graph, ctypes.c_float(0.05)) +> ``` + +--- + +### Persistence + +> Defined in `tensor_io.h` + +```c +/* Saves raw float32 data sequentially. No shape metadata stored. */ +int sc_save_model(const char *path, sc_tensor_t **tensors, size_t count); + +/* Loads raw float32 data into pre-allocated tensors (shapes must match). */ +int sc_load_model(const char *path, sc_tensor_t **tensors, size_t count); +``` + +> [!WARNING] +> Only the raw float bytes are persisted — **not** the tensor shapes or +> dtypes. You must recreate tensors with the correct shapes before +> calling `sc_load_model`. + +--- + +## Python ctypes Example + +```python +#!/usr/bin/env python3 +"""XOR training using soft-cuda from Python via ctypes.""" + +import ctypes +import os + +# ── Load the shared library ──────────────────────────────────── +lib = ctypes.CDLL("./build/libsoft_cuda_python.so") + +# ── Declare return types (ctypes defaults to c_int otherwise) ── +lib.sc_pool_create.restype = ctypes.c_void_p +lib.sc_tensor_create.restype = ctypes.c_void_p +lib.sc_tensor_get_data.restype = ctypes.c_void_p +lib.sc_tensor_get_dims.restype = ctypes.POINTER(ctypes.c_uint32) +lib.sc_tensor_mul_naive.restype = ctypes.c_void_p +lib.sc_tensor_add.restype = ctypes.c_void_p +lib.sc_tensor_relu.restype = ctypes.c_void_p +lib.sc_tensor_sub.restype = ctypes.c_void_p +lib.sc_tensor_square.restype = ctypes.c_void_p +lib.sc_tensor_mean.restype = ctypes.c_void_p +lib.sc_build_graph.restype = ctypes.c_void_p +lib.sc_graph_get_loss.restype = ctypes.c_float +lib.sc_graph_size.restype = ctypes.c_size_t + +# ── Constants ────────────────────────────────────────────────── +SC_DTYPE_FLOAT32 = 4 +SC_BACKEND_CPU = 1 + +# ── Helper to create a uint32 array ─────────────────────────── +def dims(*d): + return (ctypes.c_uint32 * len(d))(*d) + +def floats(*f): + return (ctypes.c_float * len(f))(*f) + +# ── 1. Create pools ─────────────────────────────────────────── +MB = 1024 * 1024 +pool = lib.sc_pool_create(MB, 0) +meta = lib.sc_pool_create(MB, 0) +gcpu = lib.sc_pool_create(MB, 0) +gpu = lib.sc_pool_create(MB, 1) +ggpu = lib.sc_pool_create(MB, 1) + +# ── 2. Create tensors ───────────────────────────────────────── +X = lib.sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims(4,2), + floats(0,0, 0,1, 1,0, 1,1), 0) +Y = lib.sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims(4,1), + floats(0, 1, 1, 0), 0) + +W1 = lib.sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims(2,4), None, 1) +b1 = lib.sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims(1,4), None, 1) +W2 = lib.sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims(4,1), None, 1) +b2 = lib.sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims(1,1), None, 1) + +lib.sc_tensor_fill_random_normal(W1, ctypes.c_float(0.5), ctypes.c_float(0.2)) +lib.sc_tensor_fill_random_normal(W2, ctypes.c_float(0.5), ctypes.c_float(0.2)) +lib.sc_tensor_fill_random_normal(b1, ctypes.c_float(0.0), ctypes.c_float(0.1)) +lib.sc_tensor_fill_random_normal(b2, ctypes.c_float(0.0), ctypes.c_float(0.1)) + +# ── 3. Define computation graph ─────────────────────────────── +H = lib.sc_tensor_relu(pool, lib.sc_tensor_add(pool, + lib.sc_tensor_mul_naive(pool, X, W1), b1)) +Y_pred = lib.sc_tensor_add(pool, lib.sc_tensor_mul_naive(pool, H, W2), b2) +diff = lib.sc_tensor_sub(pool, Y_pred, Y) +loss = lib.sc_tensor_mean(pool, lib.sc_tensor_square(pool, diff)) + +# ── 4. Build graph (one call) ───────────────────────────────── +graph = lib.sc_build_graph(meta, gpu, gcpu, ggpu, loss, SC_BACKEND_CPU) +assert graph is not None +print(f"Graph built: {lib.sc_graph_size(graph)} nodes") + +# ── 5. Train ────────────────────────────────────────────────── +for epoch in range(10001): + lib.sc_graph_step(pool, gpu, graph, ctypes.c_float(0.05)) + if epoch % 2000 == 0: + l = lib.sc_graph_get_loss(graph) + print(f" epoch {epoch:5d} loss = {l:.8f}") + +# ── 6. Read predictions ────────────────────────────────────── +lib.sc_graph_forward(pool, gpu, graph) +pred_ptr = ctypes.cast(lib.sc_tensor_get_data(Y_pred), + ctypes.POINTER(ctypes.c_float)) + +print("\n X1 X2 │ Target │ Predicted") +print(" " + "─" * 38) +inputs = [0,0, 0,1, 1,0, 1,1] +targets = [0, 1, 1, 0] +for i in range(4): + print(f" {inputs[i*2]:.0f} {inputs[i*2+1]:.0f} │ {targets[i]:.0f} │ {pred_ptr[i]:.4f}") + +# ── 7. Cleanup ──────────────────────────────────────────────── +lib.sc_graph_destroy(graph) +for p in [pool, meta, gcpu, ggpu, gpu]: + lib.sc_pool_destroy(p) +``` + +--- + +## C/C++ Usage Example + +See **`main.cpp`** in the repository root — it trains an XOR network +using exclusively `sc_*` calls and is heavily commented as a walkthrough. + +The key pattern is: + +```c +#include "soft-cuda/python/soft_cuda_python.h" /* single include */ + +/* 1. Pools */ +sc_pool_t *pool = sc_pool_create(1*1024*1024, 0); + +/* 2. Tensors (SC_DTYPE_FLOAT32 = 4, grad=1 for weights) */ +sc_tensor_t *W = sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims, NULL, 1); +sc_tensor_fill_random_normal(W, 0.0f, 0.1f); + +/* 3. Lazy ops */ +sc_tensor_t *out = sc_tensor_mul_naive(pool, X, W); + +/* 4. Build graph (one call) */ +sc_graph_t *g = sc_build_graph(meta, gpu, gcpu, ggpu, loss, SC_BACKEND_CPU); + +/* 5. Train */ +for (int i = 0; i < 10000; i++) + sc_graph_step(pool, gpu, g, 0.05f); + +/* 6. Read loss */ +float l = sc_graph_get_loss(g); + +/* 7. Cleanup */ +sc_graph_destroy(g); +sc_pool_destroy(pool); +``` + +--- + +## Memory Model + +soft-cuda uses **arena (bump) allocation** — all objects are allocated +from pre-sized pools: + +``` +┌─────────────────────────────────────────────────────┐ +│ sc_pool_t (e.g. 1 MB) │ +│ │ +│ ┌────┬────┬────┬────┬────┬───────────────────────┐ │ +│ │ T1 │ T2 │ T3 │ T4 │ T5 │ ← free space → │ │ +│ └────┴────┴────┴────┴────┴───────────────────────┘ │ +│ ▲ ▲ │ +│ base bump pointer │ +│ │ +│ sc_pool_zero() → resets bump to base (O(1) reset) │ +│ sc_pool_destroy() → returns memory to the OS │ +└─────────────────────────────────────────────────────┘ +``` + +**Recommended pool layout:** + +| Pool | `on_device` | Purpose | +|---|---|---| +| `pool` | 0 | Tensor data + op nodes | +| `pool_meta` | 0 | `execution_node_t` objects (graph metadata) | +| `pool_grad_cpu` | 0 | CPU-side gradient tensors | +| `pool_gpu` | 1 | GPU VRAM for forward-pass device data | +| `pool_grad_gpu` | 1 | GPU VRAM for backward-pass gradient buffers | + +> [!CAUTION] +> `sc_pool_zero()` invalidates **all** tensors allocated from that pool. +> Use it only on pools whose contents you're sure you no longer need +> (e.g. ephemeral scratch pools, not your weight pool). + +--- + +## Design Decisions + +### Why a separate bridge layer (instead of fixing `api.h` directly)? + +The existing `api.h` uses C++ features (`enum class`, `std::vector`, +default argument values) that Python's FFI (ctypes, cffi) cannot parse. +Rather than stripping those features from the core library (which would +break the natural C++ ergonomics for C++ users), we added a thin +translation layer. + +### Why `sc_graph_t` instead of exposing `std::vector` as an opaque pointer? + +A raw `std::vector*` could work, but: + +1. We'd need a separate `sc_graph_push` / `sc_graph_get` / `sc_graph_len` + anyway — the bridge functions need typed access. +2. Bundling the vector inside a named struct gives us a place to add + additional state later (e.g. caching the loss value, storing pool + references for automatic cleanup). +3. `typedef struct sc_graph sc_graph_t;` makes the intent clear in the + header — Python users see "this is a graph handle" not "void pointer + to something". + +### Why `placement-new` in `sc_graph_create`? + +`malloc` gives us a C-compatible, ABI-stable allocation. `placement-new` +properly constructs the `std::vector` inside that allocation. +This avoids exposing `new`/`delete` at the ABI boundary while ensuring +the vector's internal state is correctly initialised. + +### Why `SC_DTYPE_*` constants instead of an enum? + +Python `ctypes` can't parse C `enum` declarations. `#define` constants +are just integers — they work everywhere. The bridge function casts +them back to `tensor_dtype_t` internally. + +### Why Layer 1 + Layer 2? + +Layer 1 gives full control (e.g. run forward without backward, inspect +intermediate nodes, use a custom optimizer). Layer 2 covers the 90% +case (training loop) in fewer calls. Both are available simultaneously. diff --git a/include/soft-cuda/python/soft_cuda_python.h b/include/soft-cuda/python/soft_cuda_python.h new file mode 100644 index 0000000..0306190 --- /dev/null +++ b/include/soft-cuda/python/soft_cuda_python.h @@ -0,0 +1,48 @@ +/* ═══════════════════════════════════════════════════════════════════════════ + * Written by : Antigravity (AI Coding Assistant) + * Date : 2026-04-21 02:12 IST + * ═══════════════════════════════════════════════════════════════════════════ */ + +/** + * @file soft_cuda_python.h + * @brief Master include for the soft-cuda Python/ctypes bridge layer. + * + * Include only this header in your Python binding or cffi cdef block. + * It pulls in the five sub-headers in dependency order: + * + * tensor_pool.h — arena management (create, destroy, zero, alloc, size, used) + * tensor_core.h — tensor lifecycle (create, id, get_data, get_ndims, get_dims, + * print_data, fill_random_normal) + * tensor_ops.h — forward ops + evaluate + * (mul, mul_naive, add, add_bias, sub, relu, + * mean, mse_loss, square, transpose, evaluate, evaluate_gpu) + * tensor_graph.h — graph building + training primitives + * Layer 1: verify_dag, assign_backend, assign_grad_memory, + * graph_forward, autograd_gpu_transfer, + * grad_initializer, backward, sgd, node_to_host + * Layer 2: build_graph, graph_step, graph_get_loss, graph_size + * tensor_io.h — persistence (save_model, load_model) + * + * All symbols use the "sc_" prefix and expose only C-safe types so that + * Python ctypes / cffi can parse and call them directly. + * + * Shared library: link against libsoft_cuda_python.so (or .dll on Windows). + * + * Quick Python example: + * @code + * import ctypes, numpy as np + * lib = ctypes.CDLL("./libsoft_cuda_python.so") + * + * lib.sc_pool_create.restype = ctypes.c_void_p + * lib.sc_pool_create.argtypes = [ctypes.c_size_t, ctypes.c_int] + * pool = lib.sc_pool_create(4 * 1024 * 1024, 0) # 4 MB CPU pool + * @endcode + */ + +#pragma once + +#include "tensor_pool.h" +#include "tensor_core.h" +#include "tensor_ops.h" +#include "tensor_graph.h" +#include "tensor_io.h" diff --git a/include/soft-cuda/python/tensor_core.h b/include/soft-cuda/python/tensor_core.h new file mode 100644 index 0000000..0370b49 --- /dev/null +++ b/include/soft-cuda/python/tensor_core.h @@ -0,0 +1,113 @@ +/* ═══════════════════════════════════════════════════════════════════════════ + * Written by : Antigravity (AI Coding Assistant) + * Date : 2026-04-21 02:12 IST + * ═══════════════════════════════════════════════════════════════════════════ */ + +/** + * @file tensor_core.h + * @brief Flat-C tensor lifecycle API — Python/ctypes compatible. + * + * Uses plain int dtype constants instead of enum class tensor_dtype_t + * so Python ctypes can pass them directly. + * Include via soft_cuda_python.h — do not include directly. + */ + +#pragma once + +#include +#include "tensor_pool.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Opaque tensor handle. */ +typedef struct tensor_instance sc_tensor_t; + +/* ----------------------------------------------------------------------- + * Data-type constants (mirrors tensor_dtype_t enum class values) + * ----------------------------------------------------------------------- */ +#define SC_DTYPE_UINT32 0 +#define SC_DTYPE_INT32 1 +#define SC_DTYPE_UINT64 2 +#define SC_DTYPE_INT64 3 +#define SC_DTYPE_FLOAT32 4 +#define SC_DTYPE_FLOAT64 5 + +/* ----------------------------------------------------------------------- + * Tensor lifecycle + * ----------------------------------------------------------------------- */ + +/** + * Create a tensor. + * + * @param pool Memory pool to allocate from. + * @param dtype One of the SC_DTYPE_* constants above. + * @param num_dims Rank of the tensor (number of dimensions). + * @param dims Array of length num_dims containing each dimension size. + * @param elems Initial data buffer, or NULL for a zero-initialised tensor. + * The buffer is copied; the caller retains ownership. + * @param grad Non-zero → autograd will track this tensor. + * @return New tensor pointer, or NULL on allocation failure. + */ +sc_tensor_t *sc_tensor_create(sc_pool_t *pool, + int dtype, + uint32_t num_dims, + uint32_t *dims, + void *elems, + int grad); + +/** + * Return the unique identifier for a tensor. + * IDs are unique within the same pool. + * + * @param t The tensor. + * @return Unique uint32 id. + */ +uint32_t sc_tensor_id(sc_tensor_t *t); + +/** + * Fetch a raw pointer to the tensor's data buffer. + * + * @param t The tensor. + * @return void* to the underlying data array. + */ +void *sc_tensor_get_data(sc_tensor_t *t); + +/** + * Return the rank (number of dimensions) of the tensor. + * + * @param t The tensor. + * @return Number of dimensions (0 = scalar). + */ +uint8_t sc_tensor_get_ndims(sc_tensor_t *t); + +/** + * Return a pointer to the dimension array. + * The array has sc_tensor_get_ndims(t) valid entries. + * + * @param t The tensor. + * @return Pointer to dims[TENSOR_MAX_DIMS+1]. + */ +uint32_t *sc_tensor_get_dims(sc_tensor_t *t); + +/** + * Print tensor data to stdout (mirrors tensor_print_data). + * + * @param t The tensor to print. + */ +void sc_tensor_print_data(sc_tensor_t *t); + +/** + * Fill an existing tensor with normally distributed random floats. + * + * @param t Tensor to fill (must be FLOAT32). + * @param mean Distribution mean. + * @param std_dev Standard deviation. + * @return Non-zero on success. + */ +int sc_tensor_fill_random_normal(sc_tensor_t *t, float mean, float std_dev); + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/include/soft-cuda/python/tensor_graph.h b/include/soft-cuda/python/tensor_graph.h new file mode 100644 index 0000000..1ce9b3e --- /dev/null +++ b/include/soft-cuda/python/tensor_graph.h @@ -0,0 +1,210 @@ +/* ═══════════════════════════════════════════════════════════════════════════ + * Written by : Antigravity (AI Coding Assistant) + * Date : 2026-04-21 02:12 IST + * ═══════════════════════════════════════════════════════════════════════════ */ + +/** + * @file tensor_graph.h + * @brief Flat-C graph building and training primitives — Python/ctypes compatible. + * + * Two-layer API: + * Layer 1 — low-level primitives that mirror the internal C++ graph functions. + * Layer 2 — high-level convenience wrappers for typical Python usage. + * + * Key design: sc_graph_t is an opaque handle that wraps + * std::vector so Python never sees C++ containers. + * + * Include via soft_cuda_python.h — do not include directly. + */ + +#pragma once + +#include +#include +#include "tensor_pool.h" +#include "tensor_core.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ----------------------------------------------------------------------- + * Backend mode constants (mirrors backend_mode enum class) + * ----------------------------------------------------------------------- */ +#define SC_BACKEND_GPU 0 +#define SC_BACKEND_CPU 1 +#define SC_BACKEND_HYBRID 2 + +/* ----------------------------------------------------------------------- + * Opaque graph handle + * Internally holds a std::vector allocated on the heap. + * ----------------------------------------------------------------------- */ +typedef struct sc_graph sc_graph_t; + +/* ----------------------------------------------------------------------- + * Layer 1 — low-level graph primitives + * ----------------------------------------------------------------------- */ + +/** + * Allocate a new, empty graph handle. + * Must be freed with sc_graph_destroy when no longer needed. + * + * @return New sc_graph_t*, or NULL on allocation failure. + */ +sc_graph_t *sc_graph_create(void); + +/** + * Free the graph handle and the internal node vector. + * Does NOT free the tensors or pools themselves. + * + * @param g Graph to destroy. + */ +void sc_graph_destroy(sc_graph_t *g); + +/** + * Topologically sort the tensor computation graph starting from tensor `t` + * and populate the graph handle with execution nodes. + * Detects cycles (returns 0 if a cycle is found). + * + * @param meta_pool Pool used to allocate execution_node_t objects. + * Recommended: a dedicated metadata pool. + * @param t Root tensor (e.g. loss node). + * @param g Graph handle to populate. + * @return Non-zero on success (valid DAG), 0 if cycle detected. + */ +int sc_verify_dag(sc_pool_t *meta_pool, sc_tensor_t *t, sc_graph_t *g); + +/** + * Assign a backend (CPU / GPU / HYBRID) to each node in the graph. + * For GPU nodes, allocates device memory in pool_gpu. + * + * @param pool_gpu GPU VRAM pool for device memory allocation. + * @param g Populated graph handle. + * @param mode One of SC_BACKEND_CPU / SC_BACKEND_GPU / SC_BACKEND_HYBRID. + */ +void sc_assign_backend(sc_pool_t *pool_gpu, sc_graph_t *g, int mode); + +/** + * Allocate gradient memory for each trainable node. + * CPU grad tensors go into pool_grad_cpu; GPU grad buffers into pool_grad_gpu. + * + * @param pool_grad_cpu CPU pool for gradient tensors. + * @param pool_grad_gpu GPU pool for gradient device buffers. + * @param g Graph handle. + */ +void sc_assign_grad_memory(sc_pool_t *pool_grad_cpu, + sc_pool_t *pool_grad_gpu, + sc_graph_t *g); + +/** + * Run the full forward pass over the graph (all nodes, in topological order). + * Handles CPU↔GPU data transfer as needed by the assigned backends. + * + * @param pool_cpu CPU pool (for CPU-side evaluate calls). + * @param pool_gpu GPU pool (metadata, used by GPU evaluate). + * @param g Graph handle. + * @return Non-zero on success. + */ +int sc_graph_forward(sc_pool_t *pool_cpu, sc_pool_t *pool_gpu, sc_graph_t *g); + +/** + * Copy GPU gradient data back to CPU host memory after the forward pass. + * Must be called before sc_backward if any nodes ran on GPU. + * + * @param g Graph handle. + */ +void sc_autograd_gpu_transfer(sc_graph_t *g); + +/** + * Zero out all gradient buffers in the graph (call at the start of each step). + * + * @param g Graph handle. + */ +void sc_grad_initializer(sc_graph_t *g); + +/** + * Run backward pass (autograd) over the entire graph. + * + * @param g Graph handle. + * @return Non-zero on success. + */ +int sc_backward(sc_graph_t *g); + +/** + * Stochastic gradient descent update for all leaf (op == NONE) trainable tensors. + * + * @param g Graph handle. + * @param learning_rate Step size for parameter update. + */ +void sc_sgd(sc_graph_t *g, float learning_rate); + +/** + * Copy the data of a specific node from device (GPU) to host (CPU). + * Useful to read a specific intermediate result after the forward pass. + * + * @param g Graph handle. + * @param node_idx Index of the node in the topological order (0-based). + * @return Non-zero on success. + */ +int sc_node_to_host(sc_graph_t *g, size_t node_idx); + +/* ----------------------------------------------------------------------- + * Layer 2 — high-level convenience API + * ----------------------------------------------------------------------- */ + +/** + * Build a ready-to-train graph from a loss tensor in one call. + * Internally calls sc_verify_dag → sc_assign_backend → sc_assign_grad_memory. + * + * Caller must still supply pool_grad_cpu and pool_grad_gpu for gradient + * memory (they are passed through to sc_assign_grad_memory). + * + * @param meta_pool Pool for execution_node_t allocation. + * @param pool_gpu GPU VRAM pool (device data + grad buffers). + * @param pool_grad_cpu CPU pool for gradient tensors. + * @param pool_grad_gpu GPU pool for gradient device buffers. + * @param loss Root loss tensor (usually the output of sc_tensor_mse_loss). + * @param backend_mode One of SC_BACKEND_* constants. + * @return Ready sc_graph_t*, or NULL on failure. + */ +sc_graph_t *sc_build_graph(sc_pool_t *meta_pool, + sc_pool_t *pool_gpu, + sc_pool_t *pool_grad_cpu, + sc_pool_t *pool_grad_gpu, + sc_tensor_t *loss, + int backend_mode); + +/** + * Execute a single full training step: + * forward → autograd_gpu_transfer → grad_initializer → backward → sgd + * + * @param pool_cpu CPU execution pool. + * @param pool_gpu GPU execution pool. + * @param g Graph handle. + * @param learning_rate SGD step size. + */ +void sc_graph_step(sc_pool_t *pool_cpu, + sc_pool_t *pool_gpu, + sc_graph_t *g, + float learning_rate); + +/** + * Read the scalar loss value from the last node in the graph. + * Returns NaN if the last node is not a scalar or has not been evaluated. + * + * @param g Graph handle. + * @return float loss value. + */ +float sc_graph_get_loss(sc_graph_t *g); + +/** + * Return the number of nodes in the graph (useful for iteration / debugging). + * + * @param g Graph handle. + * @return Node count. + */ +size_t sc_graph_size(sc_graph_t *g); + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/include/soft-cuda/python/tensor_io.h b/include/soft-cuda/python/tensor_io.h new file mode 100644 index 0000000..4e81252 --- /dev/null +++ b/include/soft-cuda/python/tensor_io.h @@ -0,0 +1,58 @@ +/* ═══════════════════════════════════════════════════════════════════════════ + * Written by : Antigravity (AI Coding Assistant) + * Date : 2026-04-21 02:12 IST + * ═══════════════════════════════════════════════════════════════════════════ */ + +/** + * @file tensor_io.h + * @brief Flat-C model persistence API — Python/ctypes compatible. + * + * Wraps the C++ save_model / load_model (from src/core/graph/saveLoad.cpp) + * using only C-safe primitives (const char* path, pointer array + count). + * + * Include via soft_cuda_python.h — do not include directly. + */ + +#pragma once + +#include +#include "tensor_core.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Save model weights to a binary file. + * + * Writes all tensor data sequentially as raw float32 bytes. + * Tensors must already have been evaluated (data populated) before saving. + * + * @param path Null-terminated file path. + * @param tensors Array of tensor pointers to save (parameter tensors). + * @param count Number of tensors in the array. + * @return Non-zero on success, 0 on failure (e.g. file not writable). + * + * @note Only the raw float data is saved — shape/dtype metadata is NOT + * persisted. The caller must reconstruct tensors of the correct + * shape before calling sc_load_model. + */ +int sc_save_model(const char *path, sc_tensor_t **tensors, size_t count); + +/** + * Load model weights from a binary file into pre-allocated tensors. + * + * Reads raw float32 bytes and fills each tensor in-order. + * Tensors must already be created with the correct shapes before loading. + * + * @param path Null-terminated file path. + * @param tensors Array of pre-allocated tensor pointers to fill. + * @param count Number of tensors in the array. + * @return Non-zero on success, 0 on failure (e.g. file not found, + * size mismatch). + */ +int sc_load_model(const char *path, sc_tensor_t **tensors, size_t count); + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/include/soft-cuda/python/tensor_ops.h b/include/soft-cuda/python/tensor_ops.h new file mode 100644 index 0000000..87bb1fc --- /dev/null +++ b/include/soft-cuda/python/tensor_ops.h @@ -0,0 +1,107 @@ +/* ═══════════════════════════════════════════════════════════════════════════ + * Written by : Antigravity (AI Coding Assistant) + * Date : 2026-04-21 02:12 IST + * ═══════════════════════════════════════════════════════════════════════════ */ + +/** + * @file tensor_ops.h + * @brief Flat-C forward ops + evaluate — Python/ctypes compatible. + * + * Every function returns a new sc_tensor_t* allocated in the given pool + * (lazy / define-graph style), except sc_tensor_evaluate* which trigger + * the actual computation for a single node. + * Include via soft_cuda_python.h — do not include directly. + */ + +#pragma once + +#include "tensor_pool.h" +#include "tensor_core.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ----------------------------------------------------------------------- + * Matrix / element-wise ops (all lazy — return a new op-node tensor) + * ----------------------------------------------------------------------- */ + +/** Cache-optimised matrix multiplication (calls tensor_mul internally). */ +sc_tensor_t *sc_tensor_mul(sc_pool_t *pool, sc_tensor_t *x, sc_tensor_t *y); + +/** Naive O(n³) matrix multiplication (calls tensor_mul_naive). */ +sc_tensor_t *sc_tensor_mul_naive(sc_pool_t *pool, sc_tensor_t *x, sc_tensor_t *y); + +/** Element-wise / matrix addition. */ +sc_tensor_t *sc_tensor_add(sc_pool_t *pool, sc_tensor_t *x, sc_tensor_t *y); + +/** + * Explicit bias-broadcast addition (e.g., Y = XW + b where b has fewer dims). + * Calls tensor_add_bias internally. + */ +sc_tensor_t *sc_tensor_add_bias(sc_pool_t *pool, sc_tensor_t *xw, sc_tensor_t *bias); + +/** Element-wise / matrix subtraction. */ +sc_tensor_t *sc_tensor_sub(sc_pool_t *pool, sc_tensor_t *a, sc_tensor_t *b); + +/** ReLU activation. */ +sc_tensor_t *sc_tensor_relu(sc_pool_t *pool, sc_tensor_t *a); + +/** Scalar mean of all elements. Returns a rank-0 (scalar) tensor. */ +sc_tensor_t *sc_tensor_mean(sc_pool_t *pool, sc_tensor_t *a); + +/** + * Mean-squared-error loss. + * + * @param predictions Model output tensor. + * @param target Ground-truth tensor (same shape as predictions). + * @return Scalar MSE tensor. + */ +sc_tensor_t *sc_tensor_mse_loss(sc_pool_t *pool, + sc_tensor_t *predictions, + sc_tensor_t *target); + +/** Element-wise square. */ +sc_tensor_t *sc_tensor_square(sc_pool_t *pool, sc_tensor_t *x); + +/** + * Transpose a 2-D matrix. + * The returned tensor shares no memory with the input; it is a new node. + */ +sc_tensor_t *sc_tensor_transpose(sc_pool_t *pool, sc_tensor_t *a); + +/* ----------------------------------------------------------------------- + * Evaluate (eager execution for a single node) + * ----------------------------------------------------------------------- */ + +/** + * Evaluate a single tensor node on the CPU. + * Internally calls tensor_evaluate(pool, t, NULL, NULL, NULL). + * + * @param pool CPU memory pool. + * @param t Tensor node to evaluate. + * @return Non-zero on success. + */ +int sc_tensor_evaluate(sc_pool_t *pool, sc_tensor_t *t); + +/** + * Evaluate a single tensor node on the GPU. + * Caller is responsible for providing device pointers to parent data + * (d_a, d_b) and a device pointer for the output (d_res). + * + * @param pool CPU pool (used for metadata). + * @param t Tensor node to evaluate. + * @param d_a GPU pointer to operand A data (float*), or NULL. + * @param d_b GPU pointer to operand B data (float*), or NULL. + * @param d_res GPU pointer to output buffer (float*). + * @return Non-zero on success. + */ +int sc_tensor_evaluate_gpu(sc_pool_t *pool, + sc_tensor_t *t, + float *d_a, + float *d_b, + float *d_res); + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/include/soft-cuda/python/tensor_pool.h b/include/soft-cuda/python/tensor_pool.h new file mode 100644 index 0000000..618f3fa --- /dev/null +++ b/include/soft-cuda/python/tensor_pool.h @@ -0,0 +1,80 @@ +/* ═══════════════════════════════════════════════════════════════════════════ + * Written by : Antigravity (AI Coding Assistant) + * Date : 2026-04-21 02:12 IST + * ═══════════════════════════════════════════════════════════════════════════ */ + +/** + * @file tensor_pool.h + * @brief Flat-C arena management API — Python/ctypes compatible. + * + * All functions use only C-safe primitives: void*, size_t, int. + * This header is part of the soft_cuda_python bridge layer. + * Include via soft_cuda_python.h — do not include directly. + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Opaque alias so C and Python both see the same type name. */ +typedef struct tensor_pool_instance sc_pool_t; + +/** + * Create a new memory arena. + * + * @param capacity_bytes Total bytes to pre-allocate. + * @param on_device Non-zero → allocate on GPU VRAM; zero → CPU RAM. + * @return Pointer to the new pool, or NULL on failure. + */ +sc_pool_t *sc_pool_create(size_t capacity_bytes, int on_device); + +/** + * Completely destroy the pool and return memory to the system. + * + * @param pool Pool to destroy. Must not be used after this call. + */ +void sc_pool_destroy(sc_pool_t *pool); + +/** + * Reset the bump pointer to zero, invalidating all tensors in the pool + * without releasing the underlying memory block. + * Highly efficient for resetting temporary/gradient pools each step. + * + * @param pool Pool to reset. + */ +void sc_pool_zero(sc_pool_t *pool); + +/** + * Allocate raw bytes from the pool. + * + * @param pool The pool to allocate from. + * @param size Number of bytes to allocate. + * @param out_id Output: unique id assigned to this allocation. + * @return Pointer to the allocated block, or NULL if exhausted. + */ +void *sc_pool_alloc(sc_pool_t *pool, size_t size, uint32_t *out_id); + +/** + * Return the total capacity of the pool in bytes. + * + * @param pool The pool to query. + * @return Capacity in bytes. + */ +size_t sc_pool_size(sc_pool_t *pool); + +/** + * Return the number of bytes currently in use. + * + * @param pool The pool to query. + * @return Bytes consumed so far. + */ +size_t sc_pool_used(sc_pool_t *pool); + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/include/soft-cuda/tensor/api.h b/include/soft-cuda/tensor/api.h index 03a9edd..1a9fc17 100644 --- a/include/soft-cuda/tensor/api.h +++ b/include/soft-cuda/tensor/api.h @@ -3,9 +3,9 @@ #include #include -#ifdef __cplusplus -extern "C" { -#endif +// #ifdef __cplusplus +// extern "C" { +// #endif /////////////////////////////////////////////// @@ -46,15 +46,6 @@ typedef struct tensor_pool_instance tensor_pool_t; typedef struct execution_node execution_node_t; -/******************************************************************************* - * !!!!!!!! DISCARDED !!!!!!!! - * Discarding this in favour of vector of execution_node - * - * Opaque graph - * typedef struct tensor_graph_instance tensor_graph_t; - * ***************************************************************************** - */ -typedef struct tensor_graph_instance tensor_graph_t; /////////////////////////////////////////////// // RETURN TENSOR INFORMATION @@ -132,33 +123,24 @@ size_t tensor_pool_size(tensor_pool_t *pool); // DONE size_t tensor_pool_used(tensor_pool_t *pool); -//////////////////////////////////////////////////////////////////////////////////////////// -// WILL LIKELY BE DEPRECEATED /* - * Move the memory pool between device_type + * Move the node from device to host * - * @param device Name of the device to move the pool to. GPU/CPU @param t The - * tensor to move - * @pool Pool where the tensor will be allocated + * @param node The node which we want to move from device to host * */ - -// DEPRECEATED -// bool tensor_move_device(tensor_t *t, device_type target_device, tensor_pool_t *pool); -// NEW IMPLEMENTATION - // DONE bool execution_node_to_host(execution_node_t *node); -//////////////////////////////////////////////////////////////////////////////////////////// + /* * Fetches the data of the tensor. - * @return Returns a void pointer to the array + * @return Returns a void pointer to the data array * */ // DONE void *tensor_get_data(tensor_t *t); /* - * @return returns the dimension of the tensor + * @return the dimension of the tensor * */ // DONE uint8_t tensor_get_ndims(tensor_t *t); @@ -188,10 +170,20 @@ void tensor_print_data(tensor_t *t); * @Note tensor_matmul expects B to be transposed and * contiguous. Call tensor_transpose(B) first. * */ + // DONE tensor_t *tensor_mul(tensor_pool_t *pool, tensor_t *x, tensor_t *y); +/* + * @param pool Takes the tensor pool. + * @param x The tensor we want to square + * + * @return Squared tensor object + * */ + +// DONE tensor_t *tensor_square(tensor_pool_t *pool, tensor_t *x); + /* The naive version of matrix multiplication * @param pool Pointer to the tensor pool * @param x Pointer to the tensor which will be mulptiplied @@ -209,6 +201,7 @@ tensor_t *tensor_mul_naive(tensor_pool_t *pool, tensor_t *x, tensor_t *y); * * @return Returns a tensor object with operation set. * */ + // DONE tensor_t *tensor_transpose(tensor_pool_t *pool, tensor_t *a); @@ -220,12 +213,13 @@ tensor_t *tensor_transpose(tensor_pool_t *pool, tensor_t *a); * * @return Returns a tensor object with operation set. * */ + // DONE tensor_t *tensor_add(tensor_pool_t *pool, tensor_t *x, tensor_t *y); // Explicit broadcasting for layers (e.g., Y = XW + b) // DONE -tensor_t *tensor_add_bias(tensor_pool_t *pool, const tensor_t *xw, const tensor_t *bias); +tensor_t *tensor_add_bias(tensor_pool_t *pool, tensor_t *xw, tensor_t *bias); /* * Do matrix subtraction @@ -237,19 +231,6 @@ tensor_t *tensor_add_bias(tensor_pool_t *pool, const tensor_t *xw, const tensor_ * */ // DONE tensor_t *tensor_sub(tensor_pool_t *pool, tensor_t *a, tensor_t *b); -///////////////////////////////////////////////////////////// -/// DEPRECEATED tensor_mul operation handles it automatically -// /* -// * Do scalar matrix multiplication -// * @param out Pointer to the tensor where result will be stored -// * @param x Pointer to the tensor which will be mulptiplied -// * @param y Scalar with which the matrix will be multiplied -// * -// * */ -// // void tensor_scalar_mul(tensor_t *out, tensor_t *x, double y); -// -// upon request it can be exposed seperately -////////////////////////////////////////////////////////////// // The activation function // @params out Output tensor @@ -258,39 +239,60 @@ tensor_t *tensor_sub(tensor_pool_t *pool, tensor_t *a, tensor_t *b); // DONE tensor_t *tensor_relu(tensor_pool_t *pool, tensor_t *a); + +// Calculates the mean +// @params pool Tensor pool +// @a Tensor whose mean we want to find +// +// @return Returns the mean of the tensor // DONE tensor_t *tensor_mean(tensor_pool_t *pool, tensor_t *a); + // Compares result // return how correct we were b/w 0-1 + +// DONE tensor_t *tensor_mse_loss(tensor_pool_t *pool, tensor_t *predictions, tensor_t *target); -// // Fills an existing tensor with normally distributed random numbers. +// Fills an existing tensor with normally distributed random numbers. +// @params t Tensor we want to fill with random no. +// @params mean The mean around which we want to have the value +// @params std_dev Standard deviation + +// DONE bool tensor_fill_random_normal(tensor_t *t, float mean, float std_dev); -// + + +// TODO: HAVE TO DO THIS IF WE WANT CLASSIFICATION // // Fused operation combining Softmax and Cross-Entropy for stability // tensor_t *tensor_cross_entropy_loss(tensor_pool_t *pool, const tensor_t *predictions, // const tensor_t *targets); // *********************************************************************************** -// TODO: HAVE TO UPDATED FUNC SIG SPEC // Evalutes the operation(Forward) with depth=1 // @return boolean flag for status +// +// @note This is atomic operations and is used internally +// is for CPU computation // DONE bool tensor_evaluate( tensor_pool_t *pool,tensor_t *t, float *d_a = nullptr, float *d_b = nullptr, float *d_res= nullptr); + +// Is for GPU computation // DONE bool tensor_evaluate_GPU( tensor_pool_t *pool,tensor_t *t, float *d_a, float *d_b, float *d_res); -///////////////////////////////////////////////////////////// -// NO DESIGNING DONE HENCE NOT RECOMMENDED TO WORK AROUND USE IT JUST AS PLACEHOLDER BUT BE READY TO -// UPDATE API below it is still unstable ////////////////////// // THE BACKWARD PASS // Evalutes the operation(BACKWARD) with depth=1 // @return boolean flag for status +// @note Inplictly handle the GPU CPU transfer is a single operation bool tensor_backward(tensor_pool_t *pool, tensor_t *t); // The Optimizer +// @params nodes The graph +// @learning_rate The size of step +// void tensor_sgd(std::vector &nodes, float learning_rate); ///////////////////////////////////////////////////////////// // GRAPH OPERATIONS @@ -310,7 +312,7 @@ void tensor_sgd(std::vector &nodes, float learning_rate); ///////////////////////////////////////////////////////////////// -/* !!!!! SIGNATURE WAS CHANGED !!!!! +/* * Topologically sort the tensors, detect dependency and return a vector of . * * @params pool The data pool where the execution nodes will be @@ -327,7 +329,7 @@ bool verifyIfDAG(tensor_pool_t *pool, tensor_t *t, std::vector &nodes, backend_mode value = backend_mode::CPU); +void assignBackendGraph(tensor_pool_t *pool_gpu,std::vector &nodes, backend_mode value = backend_mode::CPU); + +/* This function is used for assigining temporary storage on GPU VRAM for GPU -> GPU part so we don't have + * transfer overhead and thrashing + * + * @params pool_grad_cpu Pool for CPU grad allocation + * @params pool_grad_gpu Pool for GPU grad allocation + * + * @params nodes The graph + * */ +//DONE +void assignGradMemory(tensor_pool_t *pool_grad_cpu, tensor_pool_t *pool_grad_gpu, std::vector &nodes); + void assignGradMemory(tensor_pool_t *pool_grad_cpu, tensor_pool_t *pool_grad_gpu, std::vector &nodes); /* @params Take execution_node_t which you wanna know @@ -345,10 +359,11 @@ void assignGradMemory(tensor_pool_t *pool_grad_cpu, tensor_pool_t *pool_grad_gpu int32_t getPosOfNode(execution_node_t *et); // Prints all data about execution_node_t as well as tensor data +// internally calls tensor print hence prints tensor data too + // DONE void printExecutionNode(execution_node_t *et); -// Previous SIGNATURE -// bool tensor_graph_build(tensor_graph_t *g, tensor_t *t); + /* * Evaluate the whole graph forward operation. @@ -361,14 +376,18 @@ bool tensor_graph_forward_evaluate(tensor_pool_t *pool_cpu, tensor_pool_t *pool_ ///////////////////////////////////////////////////////////////////////// // BACKWARD PASS FUNCTIONS +// Takes nodes and then sets the grads data to 0 using memset void gradInitializer(std::vector &nodes); +// Walk over the graph(nodes) and then run autograd bool tensor_graph_backward(std::vector &nodes); -void assignGradMemory(tensor_pool_t *pool_grad_cpu, tensor_pool_t *pool_grad_gpu, std::vector &nodes); - +// Used to transfer the gradient data from device to host void autogradGpuMemTranfer(std::vector &nodes); -#ifdef __cplusplus -} -#endif +bool save_model(const std::string& filepath, const std::vector& weights); +bool load_model(const std::string& filepath, const std::vector& weights); + +// #ifdef __cplusplus +// } +// #endif diff --git a/include/soft-cuda/tensor/tensor.h b/include/soft-cuda/tensor/tensor.h new file mode 100644 index 0000000..1870a19 --- /dev/null +++ b/include/soft-cuda/tensor/tensor.h @@ -0,0 +1,31 @@ +#pragme once + +void tensor_sgd(std::vector &nodes, float learning_rate); + +bool verifyIfDAG(tensor_pool_t *pool, tensor_t *t, std::vector &seq); + +void assignBackendGraph(tensor_pool_t *pool,std::vector &nodes, backend_mode value = backend_mode::CPU); + + +void assignGradMemory(tensor_pool_t *pool_grad_cpu, tensor_pool_t *pool_grad_gpu, std::vector &nodes); + + +bool tensor_graph_forward_evaluate(tensor_pool_t *pool_cpu, tensor_pool_t *pool_gpu, std::vector &nodes); + + +void gradInitializer(std::vector &nodes); + + +bool tensor_graph_backward(std::vector &nodes); + + +void assignGradMemory(tensor_pool_t *pool_grad_cpu, tensor_pool_t *pool_grad_gpu, std::vector &nodes); + + +void autogradGpuMemTranfer(std::vector &nodes); + + +bool save_model(const std::string& filepath, const std::vector& weights); + + +bool load_model(const std::string& filepath, const std::vector& weights); diff --git a/main.cpp b/main.cpp index f56d1cd..1620acb 100644 --- a/main.cpp +++ b/main.cpp @@ -1,127 +1,341 @@ -#include "soft-cuda/tensor/api.h" -#include "soft-cuda/tensor/debug_api.h" -#include -#include -#include +/* ═══════════════════════════════════════════════════════════════════════════ + * Written by : Antigravity (AI Coding Assistant) + * Date : 2026-04-21 02:12 IST + * ═══════════════════════════════════════════════════════════════════════════ */ -using namespace std; +/** + * soft-cuda — XOR demo using the flat-C Python bridge API + * + * This file demonstrates how to build and train a tiny neural network + * (2 → 4 → 1, XOR problem) using only the sc_* bridge functions. + * + * The exact same function calls work from Python via ctypes/cffi + * because every sc_* symbol has extern "C" linkage and uses only + * C-safe primitives (no std::vector, no enum class, no default args). + * + * Network architecture: + * + * X [4×2] + * │ + * ┌────┴────┐ + * │ matmul │ W1 [2×4] + * └────┬────┘ + * │ + * ┌────┴────┐ + * │ + b1 │ b1 [1×4] + * └────┬────┘ + * │ + * ┌────┴────┐ + * │ ReLU │ + * └────┬────┘ + * │ + * ┌────┴────┐ + * │ matmul │ W2 [4×1] + * └────┬────┘ + * │ + * ┌────┴────┐ + * │ + b2 │ b2 [1×1] + * └────┬────┘ + * │ + * Y_pred [4×1] + * │ + * ┌────┴────┐ + * │ MSE │─── Y (ground truth) + * └────┬────┘ + * │ + * loss (scalar) + */ -int main() { - tensor_pool_t *pool = tensor_pool_create(1024 * 1024); - tensor_pool_t *pool_meta = tensor_pool_create(1024 * 1024); - tensor_pool_t *pool_grad_cpu = tensor_pool_create(1024 * 1024); +#include "soft-cuda/python/soft_cuda_python.h" /* the ONLY header you need */ +#include +#include - tensor_pool_t *pool_gpu = tensor_pool_create(1024 * 1024, true); - tensor_pool_t *pool_grad_gpu = tensor_pool_create(1024 * 1024, true); +/* ═══════════════════════════════════════════════════════════════════════════ + * Helpers — nice printing without pulling in + * ═══════════════════════════════════════════════════════════════════════════ */ - assert(pool != NULL); - assert(pool_gpu != NULL); +static void banner(const char *msg) { + printf("\n"); + printf("╔══════════════════════════════════════════════╗\n"); + printf("║ %-44s║\n", msg); + printf("╚══════════════════════════════════════════════╝\n"); +} + +static void separator(void) { + printf("──────────────────────────────────────────────\n"); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * main + * ═══════════════════════════════════════════════════════════════════════════ */ + +int main(void) { + + /* ───────────────────────────────────────────── + * 1 ) CREATE MEMORY POOLS + * + * soft-cuda uses arena-style allocation. + * You need separate pools for: + * • data tensors (pool) + * • graph metadata (pool_meta) + * • GPU VRAM (pool_gpu) ← on_device = 1 + * • CPU gradients (pool_grad_cpu) + * • GPU gradients (pool_grad_gpu) ← on_device = 1 + * ───────────────────────────────────────────── */ + + banner("1. Creating memory pools"); - cout << "=========================================== \n"; - cout << "========= XOR IMPLEMENTATION (4-NEURON) ==== \n"; + sc_pool_t *pool = sc_pool_create(1024 * 1024, 0); /* 1 MB CPU */ + sc_pool_t *pool_meta = sc_pool_create(1024 * 1024, 0); /* 1 MB CPU */ + sc_pool_t *pool_grad_cpu = sc_pool_create(1024 * 1024, 0); /* 1 MB CPU */ + sc_pool_t *pool_gpu = sc_pool_create(1024 * 1024, 1); /* 1 MB VRAM */ + sc_pool_t *pool_grad_gpu = sc_pool_create(1024 * 1024, 1); /* 1 MB VRAM */ - float val_X[8]{0,0, 0,1, 1,0, 1,1}; - float val_Y[4]{0, 1, 1, 0}; - - float val_W1[8]{}; - float val_W2[4]{}; - float val_b1[4]{}; - float val_b2[1]{}; + assert(pool != NULL); + assert(pool_meta != NULL); + assert(pool_grad_cpu != NULL); + assert(pool_gpu != NULL); + assert(pool_grad_gpu != NULL); + + printf(" pool : %zu bytes total, %zu used\n", + sc_pool_size(pool), sc_pool_used(pool)); + + /* ───────────────────────────────────────────── + * 2 ) CREATE INPUT / TARGET TENSORS + * + * sc_tensor_create(pool, dtype, ndims, dims, data, grad) + * + * • dtype: SC_DTYPE_FLOAT32 (= 4) + * • grad = 0 for data tensors (X, Y) + * • grad = 1 for trainable weights (W, b) + * ───────────────────────────────────────────── */ + + banner("2. Creating data tensors"); + + /* XOR truth table */ + float val_X[] = { 0,0, 0,1, 1,0, 1,1 }; + float val_Y[] = { 0, 1, 1, 0 }; uint32_t dims_X[] = {4, 2}; uint32_t dims_Y[] = {4, 1}; - uint32_t dims_W1[] = {2, 4}; + + sc_tensor_t *X = sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims_X, val_X, 0); + sc_tensor_t *Y = sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims_Y, val_Y, 0); + + assert(X != NULL); + assert(Y != NULL); + + printf(" X tensor id = %u (ndims=%u, dims=[%u, %u])\n", + sc_tensor_id(X), + sc_tensor_get_ndims(X), + sc_tensor_get_dims(X)[0], + sc_tensor_get_dims(X)[1]); + + printf(" Y tensor id = %u (ndims=%u, dims=[%u, %u])\n", + sc_tensor_id(Y), + sc_tensor_get_ndims(Y), + sc_tensor_get_dims(Y)[0], + sc_tensor_get_dims(Y)[1]); + + /* ───────────────────────────────────────────── + * 3 ) CREATE TRAINABLE WEIGHT TENSORS + * + * Pass NULL for data → initialized to zero. + * Then fill with random normals for gradient-friendly init. + * ───────────────────────────────────────────── */ + + banner("3. Creating trainable weights"); + + uint32_t dims_W1[] = {2, 4}; /* input → hidden */ uint32_t dims_b1[] = {1, 4}; - uint32_t dims_W2[] = {4, 1}; + uint32_t dims_W2[] = {4, 1}; /* hidden → output */ uint32_t dims_b2[] = {1, 1}; - tensor_t *X = tensor_create(pool, tensor_dtype_t::FLOAT32_T, 2, dims_X, val_X, false); - tensor_t *Y = tensor_create(pool, tensor_dtype_t::FLOAT32_T, 2, dims_Y, val_Y, false); - - tensor_t *W1 = tensor_create(pool, tensor_dtype_t::FLOAT32_T, 2, dims_W1, val_W1, true); - tensor_t *W2 = tensor_create(pool, tensor_dtype_t::FLOAT32_T, 2, dims_W2, val_W2, true); - tensor_t *b1 = tensor_create(pool, tensor_dtype_t::FLOAT32_T, 2, dims_b1, val_b1, true); - tensor_t *b2 = tensor_create(pool, tensor_dtype_t::FLOAT32_T, 2, dims_b2, val_b2, true); - - tensor_fill_random_normal(W1, 0.5, 0.2); - tensor_fill_random_normal(W2, 0.5, 0.2); - tensor_fill_random_normal(b1, 0.0, 0.1); - tensor_fill_random_normal(b2, 0.0, 0.1); - - cout << "=========================================== \n"; - cout << "============== DEFINING OPS =============== \n"; - - tensor_t *H_pred_bef = tensor_mul_naive(pool, X, W1); - tensor_t *H_pred = tensor_add(pool, H_pred_bef, b1); - tensor_t *H = tensor_relu(pool, H_pred); - tensor_t *Y_pred_bef = tensor_mul_naive(pool, H, W2); - tensor_t *Y_pred = tensor_add(pool, Y_pred_bef, b2); - - tensor_t *L_sub = tensor_sub(pool, Y_pred, Y); - tensor_t *L_squ = tensor_square(pool, L_sub); - tensor_t *mse = tensor_mean(pool, L_squ); - - cout << "=========================================== \n"; - cout << "============== LAZY EVAL DONE ============= \n"; - - cout << "=========================================== \n"; - cout << "=========== PREPARING THE KITCHEN ========= \n"; - std::vector seq; - bool oki = verifyIfDAG(pool_meta, mse, seq); - assignBackendGraph(pool_gpu, seq, backend_mode::HYBRID); - std::cout << "TESTING OUT "; - assignGradMemory(pool_grad_cpu, pool_grad_gpu, seq); - // tensor_graph_forward_evaluate(pool, pool_gpu, seq); - - if (oki) { - cout << "=========================================== \n"; - cout << "============= STARTING TRAINING =========== \n"; - - for (int i = 0; i <= 10000; i++) { - tensor_graph_forward_evaluate(pool, pool_gpu, seq); - autogradGpuMemTranfer(seq); - gradInitializer(seq); - tensor_graph_backward(seq); - - if (i % 1000 == 0) { - std::cout << "EPOCH " << i << "\n"; - execution_node_t *mse_node = seq.back(); - printExecutionNode(mse_node); - } - - tensor_sgd(seq, 0.05); - } + sc_tensor_t *W1 = sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims_W1, NULL, 1); + sc_tensor_t *b1 = sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims_b1, NULL, 1); + sc_tensor_t *W2 = sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims_W2, NULL, 1); + sc_tensor_t *b2 = sc_tensor_create(pool, SC_DTYPE_FLOAT32, 2, dims_b2, NULL, 1); + + /* Xavier-ish init */ + sc_tensor_fill_random_normal(W1, 0.5f, 0.2f); + sc_tensor_fill_random_normal(W2, 0.5f, 0.2f); + sc_tensor_fill_random_normal(b1, 0.0f, 0.1f); + sc_tensor_fill_random_normal(b2, 0.0f, 0.1f); + + printf(" W1 [%u×%u] grad=ON id=%u\n", + sc_tensor_get_dims(W1)[0], sc_tensor_get_dims(W1)[1], sc_tensor_id(W1)); + printf(" b1 [%u×%u] grad=ON id=%u\n", + sc_tensor_get_dims(b1)[0], sc_tensor_get_dims(b1)[1], sc_tensor_id(b1)); + printf(" W2 [%u×%u] grad=ON id=%u\n", + sc_tensor_get_dims(W2)[0], sc_tensor_get_dims(W2)[1], sc_tensor_id(W2)); + printf(" b2 [%u×%u] grad=ON id=%u\n", + sc_tensor_get_dims(b2)[0], sc_tensor_get_dims(b2)[1], sc_tensor_id(b2)); + + /* ───────────────────────────────────────────── + * 4 ) DEFINE THE COMPUTATION GRAPH (LAZY) + * + * These calls do NOT compute anything yet — + * they construct op-nodes that record the + * operation and its operand pointers. + * + * The graph is: + * H = relu( X·W1 + b1 ) + * Y_pred = H·W2 + b2 + * loss = mean( (Y_pred − Y)² ) + * ───────────────────────────────────────────── */ + + banner("4. Defining computation graph (lazy)"); + + sc_tensor_t *H_pre = sc_tensor_mul_naive(pool, X, W1); + sc_tensor_t *H_bias = sc_tensor_add(pool, H_pre, b1); + sc_tensor_t *H = sc_tensor_relu(pool, H_bias); + + sc_tensor_t *O_pre = sc_tensor_mul_naive(pool, H, W2); + sc_tensor_t *Y_pred = sc_tensor_add(pool, O_pre, b2); + + sc_tensor_t *diff = sc_tensor_sub(pool, Y_pred, Y); + sc_tensor_t *sq = sc_tensor_square(pool, diff); + sc_tensor_t *loss = sc_tensor_mean(pool, sq); - tensor_graph_forward_evaluate(pool, pool_gpu, seq); + printf(" %d op-nodes defined (X→…→loss)\n", 8); + printf(" No computation has happened yet.\n"); - float* inputs = (float*)tensor_get_data(X); - float* targets = (float*)tensor_get_data(Y); - float* predictions = (float*)tensor_get_data(Y_pred); + /* ───────────────────────────────────────────── + * 5 ) BUILD THE GRAPH (Layer-2 one-liner) + * + * sc_build_graph does everything at once: + * 1. verifyIfDAG → topological sort + * 2. assignBackendGraph → CPU / GPU / HYBRID dispatch + * 3. assignGradMemory → allocate gradient buffers + * + * Returns a ready sc_graph_t* handle. + * ───────────────────────────────────────────── */ - cout << "=========================================== \n"; - cout << "============= XOR(4 NEURONS RESULT) ============= \n"; - cout << "X1\tX2\t|\tTarget\t|\tPredicted\n"; - cout << "---------------------------------------------------\n"; + banner("5. Building execution graph"); - for (int i = 0; i < 4; i++) { - float x1 = inputs[i * 2 + 0]; - float x2 = inputs[i * 2 + 1]; - float y_true = targets[i]; - float y_pred = predictions[i]; + sc_graph_t *graph = sc_build_graph( + pool_meta, + pool_gpu, + pool_grad_cpu, + pool_grad_gpu, + loss, + SC_BACKEND_CPU /* use SC_BACKEND_GPU or SC_BACKEND_HYBRID if CUDA is available */ + ); + assert(graph != NULL); - cout << x1 << "\t" << x2 << "\t|\t" << y_true << "\t|\t" << y_pred << "\n"; + printf(" Graph nodes : %zu\n", sc_graph_size(graph)); + printf(" Backend : CPU\n"); + printf(" Pool usage : data %zu / %zu bytes\n", + sc_pool_used(pool), sc_pool_size(pool)); + printf(" meta %zu / %zu bytes\n", + sc_pool_used(pool_meta), sc_pool_size(pool_meta)); + + /* ───────────────────────────────────────────── + * 6 ) TRAINING LOOP + * + * Option A — manual loop (Layer-1 calls): + * sc_graph_forward(...) + * sc_autograd_gpu_transfer(...) + * sc_grad_initializer(...) + * sc_backward(...) + * sc_sgd(...) + * + * Option B — one-liner (Layer-2): + * sc_graph_step(pool, pool_gpu, graph, lr) + * + * We'll use Option B for brevity. + * ───────────────────────────────────────────── */ + + banner("6. Training (10000 epochs, lr=0.05)"); + + int epochs = 10000; + float lr = 0.05f; + + for (int i = 0; i <= epochs; i++) { + + /* ── single training step ────────────── */ + sc_graph_step(pool, pool_gpu, graph, lr); + + /* ── log every 1000 epochs ───────────── */ + if (i % 1000 == 0) { + float l = sc_graph_get_loss(graph); + printf(" epoch %5d loss = %.8f\n", i, l); } - cout << "=========================================== \n"; + } + + /* ───────────────────────────────────────────── + * 7 ) INFERENCE — run one more forward pass + * and read back the predictions + * ───────────────────────────────────────────── */ + + banner("7. Final predictions"); + + sc_graph_forward(pool, pool_gpu, graph); + + float *inputs = (float *)sc_tensor_get_data(X); + float *targets = (float *)sc_tensor_get_data(Y); + float *predictions = (float *)sc_tensor_get_data(Y_pred); + + separator(); + printf(" X1 X2 │ Target │ Predicted\n"); + separator(); + + for (int i = 0; i < 4; i++) { + float x1 = inputs[i * 2 + 0]; + float x2 = inputs[i * 2 + 1]; - } else { - cout << "WARNING: DAG Verification failed. Aborting expedition.\n"; + printf(" %4.1f %4.1f │ %4.1f │ %7.4f\n", + x1, x2, targets[i], predictions[i]); } - tensor_pool_destroy(pool); - tensor_pool_destroy(pool_meta); - tensor_pool_destroy(pool_grad_cpu); - tensor_pool_destroy(pool_grad_gpu); - tensor_pool_destroy(pool_gpu); + separator(); + printf(" Final loss : %.8f\n", sc_graph_get_loss(graph)); + + /* ───────────────────────────────────────────── + * 8 ) SAVE & LOAD (round-trip demo) + * + * sc_save_model / sc_load_model persist raw + * float data — no shape metadata is stored, + * so you must recreate tensors of the correct + * shapes before loading. + * ───────────────────────────────────────────── */ + + banner("8. Save / Load model weights"); + + sc_tensor_t *weights[] = { W1, b1, W2, b2 }; + int saved = sc_save_model("xor_weights.bin", weights, 4); + printf(" save_model → %s\n", saved ? "OK" : "FAILED"); + + /* Zero out W1 to prove load really works */ + sc_tensor_fill_random_normal(W1, 0.0f, 0.0001f); + + int loaded = sc_load_model("xor_weights.bin", weights, 4); + printf(" load_model → %s\n", loaded ? "OK" : "FAILED"); + + /* Forward pass after reloading to confirm predictions match */ + sc_graph_forward(pool, pool_gpu, graph); + + printf(" Post-reload loss : %.8f\n", sc_graph_get_loss(graph)); + + /* ───────────────────────────────────────────── + * 9 ) CLEANUP + * + * Always destroy pools when done. + * sc_graph_destroy frees the graph handle + * but NOT the pools themselves. + * ───────────────────────────────────────────── */ + + banner("9. Cleanup"); + + sc_graph_destroy(graph); + + sc_pool_destroy(pool); + sc_pool_destroy(pool_meta); + sc_pool_destroy(pool_grad_cpu); + sc_pool_destroy(pool_grad_gpu); + sc_pool_destroy(pool_gpu); + + printf(" All resources freed.\n\n"); return 0; } diff --git a/src/backend_cpu/math/mse.cpp b/src/backend_cpu/math/mse.cpp index 82bba6a..6aef5c6 100644 --- a/src/backend_cpu/math/mse.cpp +++ b/src/backend_cpu/math/mse.cpp @@ -1,2 +1,8 @@ #include "internal_header.h" +tensor_t *tensor_mse_loss(tensor_pool_t *pool, tensor_t *Y_pred, tensor_t *Y) { + tensor_t *L_sub = tensor_sub(pool, Y_pred, Y); + tensor_t *L_squ = tensor_square(pool, L_sub); + tensor_t *mse = tensor_mean(pool, L_squ); + return mse; +} diff --git a/src/core/JSON/json_utils.cpp b/src/core/JSON/json_utils.cpp index fce2b4c..78b2320 100644 --- a/src/core/JSON/json_utils.cpp +++ b/src/core/JSON/json_utils.cpp @@ -18,6 +18,28 @@ readJsonToMap(const std::string& filePath) { json j; file >> j; + // Check if JSON is object + if (!j.is_object()) { + debug("JSON root is not an object"); + } + + // Convert JSON to unordered_map + for (auto& [key, value] : j.items()) { + result[key] = value; + } + + return result; +} + +std::unordered_map +readDefaultToMap(std::string_view conf) { + + std::unordered_map result; + + // Read JSON + json j = json::parse(conf); + + // Check if JSON is object if (!j.is_object()) { debug("JSON root is not an object"); diff --git a/src/core/graph/CONFIG.soft b/src/core/graph/CONFIG.soft new file mode 100644 index 0000000..03d98ee --- /dev/null +++ b/src/core/graph/CONFIG.soft @@ -0,0 +1,44 @@ +{ + "meta": { + "soft_version": "0.1.0", + "device_hash": "3f8a9c2d7b1e6f4a8c0d123456789abcdeffedcba9876543210abcdef12345678", + "generated_at": "2026-03-18T09:00:00Z" + }, + "device": { + "type": "cuda", + "compute_capability": 8.6, + "vram_mb": 8192 + }, + "ops": { + "matmul": [ + { + "min": 0, + "max": 127, + "backend": "cpu" + }, + { + "min": 128, + "max": 511, + "backend": "cuda" + }, + { + "min": 512, + "max": 4096, + "backend": "cuda" + } + ], + "relu": [ + { + "backend": "cuda" + } + ], + "add": [ + { "min": 0, "max": 127, "backend": "cpu" }, + { "min": 128, "max": 4096, "backend": "cuda" } + ] + }, + "pool": { + "device": "cuda", + "block_size": 2097152 + } +} diff --git a/src/core/graph/assignBackend.cu b/src/core/graph/assignBackend.cu index 779bd58..c4b8c41 100644 --- a/src/core/graph/assignBackend.cu +++ b/src/core/graph/assignBackend.cu @@ -1,4 +1,5 @@ #include "internal_header.h" +#include // #include "vector" #include #include @@ -92,6 +93,55 @@ void assignPlaceOnDeviceMemory(tensor_pool_t *pool, int32_t a_idx, std::vector &nodes, backend_mode backend) { setUpParentReference(nodes); if (backend == backend_mode::CPU) { @@ -103,7 +153,16 @@ void assignBackendGraph(tensor_pool_t *pool,std::vector &nod assignBackendGpu(node); } } else if (backend == backend_mode::HYBRID) { - json data = readJsonToMap("/home/wslarch/Documents/Coding/DEV/soft/soft-cuda/src/init/config/CONFIG.soft"); + json data{}; + std::string path{}; + if (getenv("HOME") != nullptr) { + static std::string path = std::string(getenv("HOME")) + "/.config/soft-cuda/CONFIG.soft"; + } + if (std::filesystem::exists(path)) { + data = readJsonToMap(path); // passing the char pointer + } else { + data = readDefaultToMap(conf); + } for (auto node : nodes) { assignBackend(node, data); } diff --git a/src/core/graph/saveLoad.cpp b/src/core/graph/saveLoad.cpp new file mode 100644 index 0000000..a9d2743 --- /dev/null +++ b/src/core/graph/saveLoad.cpp @@ -0,0 +1,36 @@ +#include "internal_header.h" + +#include +#include +#include + +bool save_model(const std::string& filepath, const std::vector& weights) { + std::ofstream out(filepath, std::ios::binary); + if (!out.is_open()) return false; + for (auto t : weights) { + uint32_t num_elements = 1; + uint8_t ndims = tensor_get_ndims(t); + uint32_t* dims = tensor_get_dims(t); + for (int i = 0; i < ndims; i++) { + num_elements *= dims[i]; + } + out.write(reinterpret_cast(tensor_get_data(t)), num_elements * sizeof(float)); + } + return true; +} + +bool load_model(const std::string& filepath, const std::vector& weights) { + std::ifstream in(filepath, std::ios::binary); + if (!in.is_open()) return false; + for (auto t : weights) { + uint32_t num_elements = 1; + uint8_t ndims = tensor_get_ndims(t); + uint32_t* dims = tensor_get_dims(t); + for (int i = 0; i < ndims; i++) { + num_elements *= dims[i]; + } + in.read(reinterpret_cast(tensor_get_data(t)), num_elements * sizeof(float)); + } + return true; +} + diff --git a/src/core/include/JSON/json_utils.h b/src/core/include/JSON/json_utils.h index 14e4ee3..24a02e9 100644 --- a/src/core/include/JSON/json_utils.h +++ b/src/core/include/JSON/json_utils.h @@ -4,3 +4,6 @@ #include "internal_header.h" std::unordered_map readJsonToMap(const std::string& filePath); + +std::unordered_map +readDefaultToMap(std::string_view conf); diff --git a/src/core/include/graph/assignBackend.h b/src/core/include/graph/assignBackend.h index 91f8d3f..2547549 100644 --- a/src/core/include/graph/assignBackend.h +++ b/src/core/include/graph/assignBackend.h @@ -4,6 +4,7 @@ using json = nlohmann::json; + void assignBackend(execution_node_t *e); device_type assignDevice(uint8_t ndims, uint32_t *dims, tensor_op_t op, uint32_t nvalues, json &data); diff --git a/src/init/config/soft_init.cpp b/src/init/config/soft_init.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/init/config/soft_init.h b/src/init/config/soft_init.h new file mode 100644 index 0000000..3ecf8ea --- /dev/null +++ b/src/init/config/soft_init.h @@ -0,0 +1 @@ +#embed "CONFIG.soft" diff --git a/src/internal_header.h b/src/internal_header.h index 625597a..bdebfab 100644 --- a/src/internal_header.h +++ b/src/internal_header.h @@ -2,6 +2,7 @@ // PUBLIC DECLARATIONS #include "../include/soft-cuda/tensor/api.h" #include "../include/soft-cuda/tensor/debug_api.h" +#include "../include/soft-cuda/python/soft_cuda_python.h" // PRIVATE DECLARATIONS #include "./core/include/tensor/tensor.h" diff --git a/src/python/sc_bridge.cpp b/src/python/sc_bridge.cpp new file mode 100644 index 0000000..c51f21e --- /dev/null +++ b/src/python/sc_bridge.cpp @@ -0,0 +1,363 @@ +/* ═══════════════════════════════════════════════════════════════════════════ + * Written by : Antigravity (AI Coding Assistant) + * Date : 2026-04-21 02:12 IST + * ═══════════════════════════════════════════════════════════════════════════ */ + +/** + * @file sc_bridge.cpp + * @brief Implementation of the soft-cuda Python C bridge. + * + * Compiled as C++ (so it can call the internal C++ API that uses + * std::vector, enum class, etc.), but all exported symbols have C linkage + * so Python ctypes / cffi can dlopen and call them by name. + * + * Build as a shared library: + * cmake --build build --target soft_cuda_python + * + * Every exported function is declared in: + * include/soft-cuda/python/soft_cuda_python.h + * + * The internal C++ API lives in: + * include/soft-cuda/tensor/api.h (referenced via internal_header.h) + */ + +// Pull in everything: public + private headers & all internal APIs +#include "../include/soft-cuda/tensor/api.h" +#include "../include/soft-cuda/python/soft_cuda_python.h" +#include "internal_header.h" + + +#include +#include /* memcpy */ +#include /* NAN */ +#include +#include /* malloc/free */ +#include + +/* ======================================================================== + * sc_graph_t — opaque handle wrapping std::vector + * ======================================================================== + * We heap-allocate the struct itself via malloc so it has a stable address + * and no C++ constructors are visible at the ABI boundary. + */ +struct sc_graph { + std::vector nodes; +}; + +/* ======================================================================== + * Helper: cast int backend constant → backend_mode enum class + * ======================================================================== */ +static backend_mode to_backend_mode(int mode) { + switch (mode) { + case 0: return backend_mode::GPU; + case 2: return backend_mode::HYBRID; + default: return backend_mode::CPU; + } +} + +/* ======================================================================== + * Helper: cast int dtype constant → tensor_dtype_t enum class + * ======================================================================== */ +static tensor_dtype_t to_tensor_dtype(int d) { + switch (d) { + case 0: return tensor_dtype_t::UINT32_T; + case 1: return tensor_dtype_t::INT32_T; + case 2: return tensor_dtype_t::UINT64_T; + case 3: return tensor_dtype_t::INT64_T; + case 5: return tensor_dtype_t::FLOAT64_T; + default: return tensor_dtype_t::FLOAT32_T; /* SC_DTYPE_FLOAT32 = 4 */ + } +} + +/* ------------------------------------------------------------------------ */ +/* Pool wrappers */ +/* ------------------------------------------------------------------------ */ + +extern "C" sc_pool_t *sc_pool_create(size_t capacity_bytes, int on_device) { + return tensor_pool_create(capacity_bytes, (bool)on_device); +} + +extern "C" void sc_pool_destroy(sc_pool_t *pool) { + tensor_pool_destroy(pool); +} + +extern "C" void sc_pool_zero(sc_pool_t *pool) { + tensor_pool_zero(pool); +} + +extern "C" void *sc_pool_alloc(sc_pool_t *pool, size_t size, uint32_t *out_id) { + return tensor_pool_alloc(pool, size, out_id); +} + +extern "C" size_t sc_pool_size(sc_pool_t *pool) { + return tensor_pool_size(pool); +} + +extern "C" size_t sc_pool_used(sc_pool_t *pool) { + return tensor_pool_used(pool); +} + +/* ------------------------------------------------------------------------ */ +/* Tensor core wrappers */ +/* ------------------------------------------------------------------------ */ + +extern "C" sc_tensor_t *sc_tensor_create(sc_pool_t *pool, + int dtype, + uint32_t num_dims, + uint32_t *dims, + void *elems, + int grad) { + return tensor_create(pool, + to_tensor_dtype(dtype), + num_dims, + dims, + elems, + (bool)grad); +} + +extern "C" uint32_t sc_tensor_id(sc_tensor_t *t) { + return tensor_id(t); +} + +extern "C" void *sc_tensor_get_data(sc_tensor_t *t) { + return tensor_get_data(t); +} + +extern "C" uint8_t sc_tensor_get_ndims(sc_tensor_t *t) { + return tensor_get_ndims(t); +} + +extern "C" uint32_t *sc_tensor_get_dims(sc_tensor_t *t) { + return tensor_get_dims(t); +} + +extern "C" void sc_tensor_print_data(sc_tensor_t *t) { + tensor_print_data(t); +} + +extern "C" int sc_tensor_fill_random_normal(sc_tensor_t *t, + float mean, + float std_dev) { + return (int)tensor_fill_random_normal(t, mean, std_dev); +} + +/* ------------------------------------------------------------------------ */ +/* Op wrappers */ +/* ------------------------------------------------------------------------ */ + +extern "C" sc_tensor_t *sc_tensor_mul(sc_pool_t *pool, + sc_tensor_t *x, + sc_tensor_t *y) { + return tensor_mul(pool, x, y); +} + +extern "C" sc_tensor_t *sc_tensor_mul_naive(sc_pool_t *pool, + sc_tensor_t *x, + sc_tensor_t *y) { + return tensor_mul_naive(pool, x, y); +} + +extern "C" sc_tensor_t *sc_tensor_add(sc_pool_t *pool, + sc_tensor_t *x, + sc_tensor_t *y) { + return tensor_add(pool, x, y); +} + +extern "C" sc_tensor_t *sc_tensor_add_bias(sc_pool_t *pool, + sc_tensor_t *xw, + sc_tensor_t *bias) { + return tensor_add_bias(pool, xw, bias); +} + +extern "C" sc_tensor_t *sc_tensor_sub(sc_pool_t *pool, + sc_tensor_t *a, + sc_tensor_t *b) { + return tensor_sub(pool, a, b); +} + +extern "C" sc_tensor_t *sc_tensor_relu(sc_pool_t *pool, sc_tensor_t *a) { + return tensor_relu(pool, a); +} + +extern "C" sc_tensor_t *sc_tensor_mean(sc_pool_t *pool, sc_tensor_t *a) { + return tensor_mean(pool, a); +} + +extern "C" sc_tensor_t *sc_tensor_mse_loss(sc_pool_t *pool, + sc_tensor_t *predictions, + sc_tensor_t *target) { + return tensor_mse_loss(pool, predictions, target); +} + +extern "C" sc_tensor_t *sc_tensor_square(sc_pool_t *pool, sc_tensor_t *x) { + return tensor_square(pool, x); +} + +extern "C" sc_tensor_t *sc_tensor_transpose(sc_pool_t *pool, sc_tensor_t *a) { + return tensor_transpose(pool, a); +} + +extern "C" int sc_tensor_evaluate(sc_pool_t *pool, sc_tensor_t *t) { + /* Pass null device pointers — CPU path */ + return (int)tensor_evaluate(pool, t, nullptr, nullptr, nullptr); +} + +extern "C" int sc_tensor_evaluate_gpu(sc_pool_t *pool, + sc_tensor_t *t, + float *d_a, + float *d_b, + float *d_res) { + return (int)tensor_evaluate_GPU(pool, t, d_a, d_b, d_res); +} + +/* ------------------------------------------------------------------------ */ +/* Graph — Layer 1 */ +/* ------------------------------------------------------------------------ */ + +extern "C" sc_graph_t *sc_graph_create(void) { + sc_graph_t *g = (sc_graph_t *)malloc(sizeof(sc_graph_t)); + if (g == nullptr) return nullptr; + /* Placement-new to properly construct the std::vector */ + new (&g->nodes) std::vector(); + return g; +} + +extern "C" void sc_graph_destroy(sc_graph_t *g) { + if (g == nullptr) return; + /* Explicitly destroy the vector before freeing the raw memory */ + g->nodes.~vector(); + free(g); +} + +extern "C" int sc_verify_dag(sc_pool_t *meta_pool, + sc_tensor_t *t, + sc_graph_t *g) { + if (g == nullptr) return 0; + return (int)verifyIfDAG(meta_pool, t, g->nodes); +} + +extern "C" void sc_assign_backend(sc_pool_t *pool_gpu, + sc_graph_t *g, + int mode) { + if (g == nullptr) return; + assignBackendGraph(pool_gpu, g->nodes, to_backend_mode(mode)); +} + +extern "C" void sc_assign_grad_memory(sc_pool_t *pool_grad_cpu, + sc_pool_t *pool_grad_gpu, + sc_graph_t *g) { + if (g == nullptr) return; + assignGradMemory(pool_grad_cpu, pool_grad_gpu, g->nodes); +} + +extern "C" int sc_graph_forward(sc_pool_t *pool_cpu, + sc_pool_t *pool_gpu, + sc_graph_t *g) { + if (g == nullptr) return 0; + return (int)tensor_graph_forward_evaluate(pool_cpu, pool_gpu, g->nodes); +} + +extern "C" void sc_autograd_gpu_transfer(sc_graph_t *g) { + if (g == nullptr) return; + autogradGpuMemTranfer(g->nodes); +} + +extern "C" void sc_grad_initializer(sc_graph_t *g) { + if (g == nullptr) return; + gradInitializer(g->nodes); +} + +extern "C" int sc_backward(sc_graph_t *g) { + if (g == nullptr) return 0; + return (int)tensor_graph_backward(g->nodes); +} + +extern "C" void sc_sgd(sc_graph_t *g, float learning_rate) { + if (g == nullptr) return; + tensor_sgd(g->nodes, learning_rate); +} + +extern "C" int sc_node_to_host(sc_graph_t *g, size_t node_idx) { + if (g == nullptr || node_idx >= g->nodes.size()) return 0; + return (int)execution_node_to_host(g->nodes[node_idx]); +} + +/* ------------------------------------------------------------------------ */ +/* Graph — Layer 2 (convenience wrappers) */ +/* ------------------------------------------------------------------------ */ + +extern "C" sc_graph_t *sc_build_graph(sc_pool_t *meta_pool, + sc_pool_t *pool_gpu, + sc_pool_t *pool_grad_cpu, + sc_pool_t *pool_grad_gpu, + sc_tensor_t *loss, + int backend_mode_val) { + sc_graph_t *g = sc_graph_create(); + if (g == nullptr) return nullptr; + + if (!verifyIfDAG(meta_pool, loss, g->nodes)) { + sc_graph_destroy(g); + return nullptr; + } + + assignBackendGraph(pool_gpu, g->nodes, to_backend_mode(backend_mode_val)); + assignGradMemory(pool_grad_cpu, pool_grad_gpu, g->nodes); + + return g; +} + +extern "C" void sc_graph_step(sc_pool_t *pool_cpu, + sc_pool_t *pool_gpu, + sc_graph_t *g, + float learning_rate) { + if (g == nullptr) return; + tensor_graph_forward_evaluate(pool_cpu, pool_gpu, g->nodes); + autogradGpuMemTranfer(g->nodes); + gradInitializer(g->nodes); + tensor_graph_backward(g->nodes); + tensor_sgd(g->nodes, learning_rate); +} + +extern "C" float sc_graph_get_loss(sc_graph_t *g) { + if (g == nullptr || g->nodes.empty()) return NAN; + + execution_node_t *last = g->nodes.back(); + if (last == nullptr || last->t == nullptr) return NAN; + + /* Bring the loss node back to host if it ran on GPU */ + if (last->t->device == device_type::GPU) { + execution_node_to_host(last); + } + + if (last->t->data == nullptr) return NAN; + + /* Loss node must be a scalar (1 element) */ + float val; + memcpy(&val, last->t->data, sizeof(float)); + return val; +} + +extern "C" size_t sc_graph_size(sc_graph_t *g) { + if (g == nullptr) return 0; + return g->nodes.size(); +} + +/* ------------------------------------------------------------------------ */ +/* IO wrappers */ +/* ------------------------------------------------------------------------ */ + +extern "C" int sc_save_model(const char *path, + sc_tensor_t **tensors, + size_t count) { + if (path == nullptr || tensors == nullptr) return 0; + std::vector vec(tensors, tensors + count); + return (int)save_model(std::string(path), vec); +} + +extern "C" int sc_load_model(const char *path, + sc_tensor_t **tensors, + size_t count) { + if (path == nullptr || tensors == nullptr) return 0; + std::vector vec(tensors, tensors + count); + return (int)load_model(std::string(path), vec); +}