Current section
Files
Jump to
Current section
Files
c_src/mlx_streaming_nif.cpp
#include <erl_nif.h>
#include <mlx/mlx.h>
#include <mlx/ops.h>
#include <mlx/array.h>
#include <mlx/device.h>
#include <mlx/dtype.h>
#include <mlx/stream.h>
#include <memory>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <atomic>
#include <future>
#include <unordered_map>
#include <chrono>
using namespace mlx::core;
namespace mx = mlx::core;
// Advanced streaming and async resource types
static ErlNifResourceType* ASYNC_OPERATION_RESOURCE_TYPE;
static ErlNifResourceType* STREAM_PIPELINE_RESOURCE_TYPE;
static ErlNifResourceType* OPERATION_QUEUE_RESOURCE_TYPE;
// Async operation types
enum class AsyncOpType {
MATMUL,
ATTENTION,
CONVOLUTION,
FFN,
CUSTOM_KERNEL
};
// Async operation wrapper
struct AsyncOperationResource {
std::future<array> future_result;
AsyncOpType op_type;
std::string operation_id;
std::chrono::steady_clock::time_point start_time;
bool is_completed;
AsyncOperationResource(std::future<array>&& fut, AsyncOpType type, const std::string& id)
: future_result(std::move(fut)), op_type(type), operation_id(id),
start_time(std::chrono::steady_clock::now()), is_completed(false) {}
};
// Stream pipeline for real-time processing
struct StreamPipelineResource {
Stream primary_stream;
Stream secondary_stream;
std::queue<std::function<array()>> operation_queue;
std::mutex queue_mutex;
std::condition_variable queue_cv;
std::atomic<bool> is_running{true};
std::thread worker_thread;
StreamPipelineResource(const Stream& primary, const Stream& secondary)
: primary_stream(primary), secondary_stream(secondary) {
worker_thread = std::thread(&StreamPipelineResource::process_queue, this);
}
~StreamPipelineResource() {
is_running = false;
queue_cv.notify_all();
if (worker_thread.joinable()) {
worker_thread.join();
}
}
void process_queue() {
while (is_running) {
std::unique_lock<std::mutex> lock(queue_mutex);
queue_cv.wait(lock, [this] { return !operation_queue.empty() || !is_running; });
if (!is_running) break;
if (!operation_queue.empty()) {
auto operation = operation_queue.front();
operation_queue.pop();
lock.unlock();
// Execute operation asynchronously
try {
array result = operation();
eval(result);
} catch (const std::exception& e) {
// Log error but continue processing
}
}
}
}
};
// Operation queue for batch processing
struct OperationQueueResource {
std::vector<std::function<array()>> batched_operations;
std::mutex operations_mutex;
size_t max_batch_size;
std::chrono::milliseconds max_wait_time;
OperationQueueResource(size_t batch_size, std::chrono::milliseconds wait_time)
: max_batch_size(batch_size), max_wait_time(wait_time) {}
};
// Global async operation manager
static std::unordered_map<std::string, std::shared_ptr<AsyncOperationResource>> async_operations;
static std::mutex async_ops_mutex;
static std::atomic<size_t> operation_counter{0};
// Helper functions
static ERL_NIF_TERM make_atom(ErlNifEnv* env, const char* name) {
ERL_NIF_TERM ret;
if (enif_make_existing_atom(env, name, &ret, ERL_NIF_LATIN1)) {
return ret;
}
return enif_make_atom(env, name);
}
static ERL_NIF_TERM make_error(ErlNifEnv* env, const char* reason) {
return enif_make_tuple2(env, make_atom(env, "error"), make_atom(env, reason));
}
static ERL_NIF_TERM make_ok(ErlNifEnv* env, ERL_NIF_TERM term) {
return enif_make_tuple2(env, make_atom(env, "ok"), term);
}
// Create async operation for non-blocking execution
static ERL_NIF_TERM mlx_async_matmul(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 2) return enif_make_badarg(env);
// Extract array resources (implementation would parse these from argv)
// For now, simulate with placeholder
try {
std::string op_id = "async_matmul_" + std::to_string(operation_counter++);
// Create async operation
auto async_op = std::async(std::launch::async, []() -> array {
// Simulate async matrix multiplication
array a = ones({1024, 1024}, float32);
array b = ones({1024, 1024}, float32);
array result = matmul(a, b);
eval(result);
return result;
});
auto resource = std::make_shared<AsyncOperationResource>(
std::move(async_op), AsyncOpType::MATMUL, op_id);
{
std::lock_guard<std::mutex> lock(async_ops_mutex);
async_operations[op_id] = resource;
}
// Return operation ID for later retrieval
ERL_NIF_TERM op_id_term = enif_make_string(env, op_id.c_str(), ERL_NIF_LATIN1);
return make_ok(env, op_id_term);
} catch (const std::exception& e) {
return make_error(env, "async_matmul_error");
}
}
// Check status of async operation
static ERL_NIF_TERM mlx_async_status(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 1) return enif_make_badarg(env);
char op_id[128];
if (!enif_get_string(env, argv[0], op_id, sizeof(op_id), ERL_NIF_LATIN1)) {
return enif_make_badarg(env);
}
try {
std::lock_guard<std::mutex> lock(async_ops_mutex);
auto it = async_operations.find(std::string(op_id));
if (it == async_operations.end()) {
return make_error(env, "operation_not_found");
}
auto& resource = it->second;
auto status = resource->future_result.wait_for(std::chrono::seconds(0));
if (status == std::future_status::ready) {
return make_ok(env, make_atom(env, "completed"));
} else {
auto elapsed = std::chrono::steady_clock::now() - resource->start_time;
auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
ERL_NIF_TERM status_tuple = enif_make_tuple2(env,
make_atom(env, "running"),
enif_make_long(env, elapsed_ms));
return make_ok(env, status_tuple);
}
} catch (const std::exception& e) {
return make_error(env, "async_status_error");
}
}
// Get result of completed async operation
static ERL_NIF_TERM mlx_async_result(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 1) return enif_make_badarg(env);
char op_id[128];
if (!enif_get_string(env, argv[0], op_id, sizeof(op_id), ERL_NIF_LATIN1)) {
return enif_make_badarg(env);
}
try {
std::lock_guard<std::mutex> lock(async_ops_mutex);
auto it = async_operations.find(std::string(op_id));
if (it == async_operations.end()) {
return make_error(env, "operation_not_found");
}
auto& resource = it->second;
if (resource->future_result.wait_for(std::chrono::seconds(0)) != std::future_status::ready) {
return make_error(env, "operation_not_completed");
}
// Get the result and create array resource
array result = resource->future_result.get();
// Create array resource wrapper (simplified)
ERL_NIF_TERM result_term = make_atom(env, "array_result");
// Remove completed operation
async_operations.erase(it);
return make_ok(env, result_term);
} catch (const std::exception& e) {
return make_error(env, "async_result_error");
}
}
// Create streaming pipeline for real-time processing
static ERL_NIF_TERM mlx_create_stream_pipeline(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 2) return enif_make_badarg(env);
char primary_device[32], secondary_device[32];
if (!enif_get_atom(env, argv[0], primary_device, sizeof(primary_device), ERL_NIF_LATIN1) ||
!enif_get_atom(env, argv[1], secondary_device, sizeof(secondary_device), ERL_NIF_LATIN1)) {
return enif_make_badarg(env);
}
try {
Device primary = (strcmp(primary_device, "gpu") == 0) ? Device::gpu : Device::cpu;
Device secondary = (strcmp(secondary_device, "gpu") == 0) ? Device::gpu : Device::cpu;
Stream primary_stream = new_stream(primary);
Stream secondary_stream = new_stream(secondary);
StreamPipelineResource* res = (StreamPipelineResource*)enif_alloc_resource(
STREAM_PIPELINE_RESOURCE_TYPE, sizeof(StreamPipelineResource));
new(res) StreamPipelineResource(primary_stream, secondary_stream);
ERL_NIF_TERM term = enif_make_resource(env, res);
enif_release_resource(res);
return make_ok(env, term);
} catch (const std::exception& e) {
return make_error(env, "stream_pipeline_error");
}
}
// Queue operation in streaming pipeline
static ERL_NIF_TERM mlx_queue_stream_operation(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 3) return enif_make_badarg(env);
StreamPipelineResource* pipeline_res;
if (!enif_get_resource(env, argv[0], STREAM_PIPELINE_RESOURCE_TYPE, (void**)&pipeline_res)) {
return enif_make_badarg(env);
}
char operation_type[64];
if (!enif_get_atom(env, argv[1], operation_type, sizeof(operation_type), ERL_NIF_LATIN1)) {
return enif_make_badarg(env);
}
try {
std::function<array()> operation;
if (strcmp(operation_type, "attention_step") == 0) {
operation = []() -> array {
// Simulate attention computation step
array q = random::normal(0, 1, {1, 64, 512}, float32);
array k = random::normal(0, 1, {1, 64, 512}, float32);
array v = random::normal(0, 1, {1, 64, 512}, float32);
array scores = matmul(q, transpose(k, {-2, -1}));
array attn = softmax(scores / sqrt(array(512.0f)), -1);
return matmul(attn, v);
};
} else if (strcmp(operation_type, "ffn_step") == 0) {
operation = []() -> array {
// Simulate feed-forward step
array x = random::normal(0, 1, {1, 64, 512}, float32);
array w1 = random::normal(0, 1, {512, 2048}, float32);
array w2 = random::normal(0, 1, {2048, 512}, float32);
array hidden = gelu(matmul(x, w1));
return matmul(hidden, w2);
};
} else {
return make_error(env, "unknown_operation_type");
}
// Queue the operation
{
std::lock_guard<std::mutex> lock(pipeline_res->queue_mutex);
pipeline_res->operation_queue.push(operation);
}
pipeline_res->queue_cv.notify_one();
return make_ok(env, make_atom(env, "operation_queued"));
} catch (const std::exception& e) {
return make_error(env, "queue_operation_error");
}
}
// Batch operations for improved throughput
static ERL_NIF_TERM mlx_create_batch_queue(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 2) return enif_make_badarg(env);
int batch_size, wait_time_ms;
if (!enif_get_int(env, argv[0], &batch_size) ||
!enif_get_int(env, argv[1], &wait_time_ms)) {
return enif_make_badarg(env);
}
try {
OperationQueueResource* res = (OperationQueueResource*)enif_alloc_resource(
OPERATION_QUEUE_RESOURCE_TYPE, sizeof(OperationQueueResource));
new(res) OperationQueueResource(batch_size, std::chrono::milliseconds(wait_time_ms));
ERL_NIF_TERM term = enif_make_resource(env, res);
enif_release_resource(res);
return make_ok(env, term);
} catch (const std::exception& e) {
return make_error(env, "batch_queue_error");
}
}
// Execute batched operations with optimized scheduling
static ERL_NIF_TERM mlx_execute_batch(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 1) return enif_make_badarg(env);
OperationQueueResource* queue_res;
if (!enif_get_resource(env, argv[0], OPERATION_QUEUE_RESOURCE_TYPE, (void**)&queue_res)) {
return enif_make_badarg(env);
}
try {
std::lock_guard<std::mutex> lock(queue_res->operations_mutex);
if (queue_res->batched_operations.empty()) {
return make_ok(env, make_atom(env, "no_operations"));
}
// Execute all batched operations in parallel
std::vector<std::future<array>> futures;
for (auto& operation : queue_res->batched_operations) {
futures.push_back(std::async(std::launch::async, operation));
}
// Wait for all operations to complete
std::vector<array> results;
for (auto& future : futures) {
results.push_back(future.get());
}
// Clear the batch
queue_res->batched_operations.clear();
return make_ok(env, enif_make_int(env, results.size()));
} catch (const std::exception& e) {
return make_error(env, "batch_execution_error");
}
}
// Real-time inference pipeline
static ERL_NIF_TERM mlx_realtime_inference(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 3) return enif_make_badarg(env);
char model_type[64];
int max_tokens, beam_width;
if (!enif_get_atom(env, argv[0], model_type, sizeof(model_type), ERL_NIF_LATIN1) ||
!enif_get_int(env, argv[1], &max_tokens) ||
!enif_get_int(env, argv[2], &beam_width)) {
return enif_make_badarg(env);
}
try {
// Set up real-time inference pipeline optimized for Apple Silicon
if (strcmp(model_type, "transformer") == 0) {
// Create transformer inference pipeline with KV caching
auto pipeline_id = "realtime_transformer_" + std::to_string(operation_counter++);
// Simulate creating inference pipeline
auto inference_future = std::async(std::launch::async, [max_tokens, beam_width]() -> array {
// Simulate real-time transformer inference
for (int token = 0; token < max_tokens; token++) {
// Simulate token generation with beam search
array logits = random::normal(0, 1, {beam_width, 32000}, float32);
array probs = softmax(logits, -1);
// Sample next token
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Simulate processing time
}
return ones({max_tokens}, int32);
});
ERL_NIF_TERM pipeline_id_term = enif_make_string(env, pipeline_id.c_str(), ERL_NIF_LATIN1);
return make_ok(env, pipeline_id_term);
}
return make_error(env, "unsupported_model_type");
} catch (const std::exception& e) {
return make_error(env, "realtime_inference_error");
}
}
// Resource destructors
static void async_operation_resource_destructor(ErlNifEnv* env, void* obj) {
AsyncOperationResource* res = (AsyncOperationResource*)obj;
res->~AsyncOperationResource();
}
static void stream_pipeline_resource_destructor(ErlNifEnv* env, void* obj) {
StreamPipelineResource* res = (StreamPipelineResource*)obj;
res->~StreamPipelineResource();
}
static void operation_queue_resource_destructor(ErlNifEnv* env, void* obj) {
OperationQueueResource* res = (OperationQueueResource*)obj;
res->~OperationQueueResource();
}
// Streaming NIF function table
static ErlNifFunc nif_funcs[] = {
// Async operations
{"async_matmul", 2, mlx_async_matmul, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"async_status", 1, mlx_async_status, 0},
{"async_result", 1, mlx_async_result, 0},
// Streaming pipelines
{"create_stream_pipeline", 2, mlx_create_stream_pipeline, 0},
{"queue_stream_operation", 3, mlx_queue_stream_operation, 0},
// Batch processing
{"create_batch_queue", 2, mlx_create_batch_queue, 0},
{"execute_batch", 1, mlx_execute_batch, ERL_NIF_DIRTY_JOB_CPU_BOUND},
// Real-time inference
{"realtime_inference", 3, mlx_realtime_inference, ERL_NIF_DIRTY_JOB_CPU_BOUND}
};
static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) {
ErlNifResourceFlags flags = (ErlNifResourceFlags)(ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER);
ErlNifResourceFlags* tried = NULL;
ASYNC_OPERATION_RESOURCE_TYPE = enif_open_resource_type(
env, NULL, "mlx_async_operation", async_operation_resource_destructor, flags, tried);
STREAM_PIPELINE_RESOURCE_TYPE = enif_open_resource_type(
env, NULL, "mlx_stream_pipeline", stream_pipeline_resource_destructor, flags, tried);
OPERATION_QUEUE_RESOURCE_TYPE = enif_open_resource_type(
env, NULL, "mlx_operation_queue", operation_queue_resource_destructor, flags, tried);
if (!ASYNC_OPERATION_RESOURCE_TYPE || !STREAM_PIPELINE_RESOURCE_TYPE || !OPERATION_QUEUE_RESOURCE_TYPE) {
return 1;
}
return 0;
}
static int upgrade(ErlNifEnv* env, void** priv_data, void** old_priv_data, ERL_NIF_TERM load_info) {
return load(env, priv_data, load_info);
}
static void unload(ErlNifEnv* env, void* priv_data) {
// Clean up async operations
std::lock_guard<std::mutex> lock(async_ops_mutex);
async_operations.clear();
}
ERL_NIF_INIT(mlx_streaming_nif, nif_funcs, load, NULL, upgrade, unload)