Download server.cpp from elyanlabs/vulkan-matmul: direct link, hf CLI and curl.
- Browser
- Download file 35.4 kB
-
https://huggingface.co/elyanlabs/vulkan-matmul/resolve/main/server.cpp
- Command line
-
hf download hf://elyanlabs/vulkan-matmul/server.cpp
-
curl -L -o server.cpp https://huggingface.co/elyanlabs/vulkan-matmul/resolve/main/server.cpp
35.4 kB
| /* | |
| * vulkan_matmul_server.cpp v3 β Single-Submit Fast Path | |
| * | |
| * Weight matrices (B) are cached in Navi 12's 8GB VRAM. | |
| * Per-token cost: only send activation A (~28KB) + get result C (~28KB). | |
| * Weight upload happens ONCE per unique weight tensor, then LRU-cached. | |
| * | |
| * v3 adds compute_cached_fast(): for warm cache hits (a_only=1), the entire | |
| * sequence (upload A β dispatch β download C) runs in ONE command buffer with | |
| * ONE vkQueueSubmit+vkWaitForFences. v2 used two submits (~43ms floor); | |
| * v3 targets ~22ms floor by eliminating the extra fence-wait round-trip. | |
| * | |
| * Fast path condition: szA + szC β€ stageSize (always true for M=1..~2000) | |
| * | |
| * Protocol v2 (32-byte header): | |
| * magic(4) M(4) N(4) K(4) type(4) a_only(4) key_lo(4) key_hi(4) | |
| * | |
| * type=0 Plain matmul β send A + B, get C (no caching) | |
| * type=1 Cached matmul: | |
| * a_only=0 β send A + B (client says "weight may not be cached") | |
| * a_only=1 β send A only (client says "weight should be in cache") | |
| * | |
| * Response (16 bytes): magic(4) status(4) M(4) N(4) then C[M*N*4] | |
| * status=0 OK | |
| * status=2 NEED_WEIGHT β client must retry with a_only=0 | |
| * | |
| * Build: cmake -B build . && cmake --build build -j16 | |
| * Run: | |
| * VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/radeon_icd.ppc64le.json \ | |
| * ./build/vulkan_matmul_server 8097 | |
| */ | |
| /* ββββββββββββββββββ helpers ββββββββββββββββββ */ | |
| static double now_ms(void) { | |
| struct timespec ts; | |
| clock_gettime(CLOCK_MONOTONIC, &ts); | |
| return ts.tv_sec * 1e3 + ts.tv_nsec * 1e-6; | |
| } | |
| static void die(const char *m) { fprintf(stderr,"FATAL: %s\n",m); exit(1); } | |
| static int recv_all(int fd, void *buf, size_t n) { | |
| uint8_t *p = (uint8_t*)buf; size_t left = n; | |
| while (left) { ssize_t r = recv(fd,p,left,MSG_WAITALL); if(r<=0) return -1; p+=r; left-=r; } | |
| return 0; | |
| } | |
| static int send_all(int fd, const void *buf, size_t n) { | |
| const uint8_t *p = (const uint8_t*)buf; size_t left = n; | |
| while (left) { ssize_t r = send(fd,p,left,0); if(r<=0) return -1; p+=r; left-=r; } | |
| return 0; | |
| } | |
| static uint8_t *load_file(const char *path, size_t *sz) { | |
| FILE *f = fopen(path,"rb"); if(!f){perror(path);return NULL;} | |
| fseek(f,0,SEEK_END); *sz=ftell(f); rewind(f); | |
| uint8_t *b = (uint8_t*)malloc(*sz); fread(b,1,*sz,f); fclose(f); | |
| return b; | |
| } | |
| /* ββββββββββββββββββ Vulkan state ββββββββββββββββββ */ | |
| typedef struct { | |
| VkInstance instance; | |
| VkPhysicalDevice phys; | |
| VkDevice device; | |
| VkQueue queue; | |
| uint32_t queueFamily; | |
| VkShaderModule shaderModule; | |
| VkDescriptorSetLayout dsetLayout; | |
| VkPipelineLayout pipeLayout; | |
| VkPipeline pipeline; | |
| VkCommandPool cmdPool; | |
| VkCommandBuffer cmdBuf; | |
| VkDescriptorPool dPool; | |
| VkDescriptorSet dSet; | |
| VkFence fence; | |
| /* Staging buffer (host-visible, BAR-mapped, persistent) */ | |
| VkBuffer stageBuf; | |
| VkDeviceMemory stageMem; | |
| void *stageMapped; | |
| size_t stageSize; | |
| /* Dynamic device-local buffers for A and C (re-alloc as needed) */ | |
| VkBuffer bufA, bufC; | |
| VkDeviceMemory memA, memC; | |
| size_t capA, capC; | |
| } VkCtx; | |
| /* ββββββββββββββββββ Weight cache ββββββββββββββββββ */ | |
| typedef struct { | |
| uint64_t key; /* (uint64_t)src0->data in llama.cpp */ | |
| VkBuffer buf; | |
| VkDeviceMemory mem; | |
| size_t bytes; | |
| uint32_t K, N; /* weight shape: [K, N] */ | |
| uint64_t last_used; /* LRU sequence counter */ | |
| int valid; | |
| } WtEntry; | |
| static WtEntry g_wt[MAX_CACHE]; | |
| static size_t g_wt_used_bytes = 0; | |
| static uint64_t g_wt_seq = 0; | |
| static size_t g_wt_budget = (size_t)CACHE_VRAM_MB * 1024 * 1024; | |
| static VkCtx g; | |
| /* Cache stats */ | |
| static uint64_t g_cache_hits = 0; | |
| static uint64_t g_cache_misses = 0; | |
| static uint64_t g_evictions = 0; | |
| /* ββββββββββββββββββ Memory helpers ββββββββββββββββββ */ | |
| static uint32_t findMemType(uint32_t typeBits, VkMemoryPropertyFlags props) { | |
| VkPhysicalDeviceMemoryProperties mp; | |
| vkGetPhysicalDeviceMemoryProperties(g.phys, &mp); | |
| for (uint32_t i = 0; i < mp.memoryTypeCount; i++) | |
| if ((typeBits & (1u<<i)) && (mp.memoryTypes[i].propertyFlags & props) == props) | |
| return i; | |
| die("No suitable memory type"); return 0; | |
| } | |
| static void allocDevBuf(VkDeviceSize sz, VkBufferUsageFlags usage, | |
| VkBuffer *buf, VkDeviceMemory *mem) { | |
| VkBufferCreateInfo bi = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; | |
| bi.size = sz; | |
| bi.usage = usage | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT; | |
| bi.sharingMode = VK_SHARING_MODE_EXCLUSIVE; | |
| vkCreateBuffer(g.device, &bi, NULL, buf); | |
| VkMemoryRequirements mr; | |
| vkGetBufferMemoryRequirements(g.device, *buf, &mr); | |
| VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; | |
| ai.allocationSize = mr.size; | |
| ai.memoryTypeIndex = findMemType(mr.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); | |
| if (vkAllocateMemory(g.device, &ai, NULL, mem) != VK_SUCCESS) { | |
| fprintf(stderr, "[vk] vkAllocateMemory failed for %zu MB\n", (size_t)sz>>20); | |
| *mem = VK_NULL_HANDLE; | |
| return; | |
| } | |
| vkBindBufferMemory(g.device, *buf, *mem, 0); | |
| } | |
| static void ensureDevBuf(size_t needed, VkBufferUsageFlags usage, | |
| VkBuffer *buf, VkDeviceMemory *mem, size_t *cap) { | |
| if (needed <= *cap) return; | |
| if (*cap > 0) { | |
| vkDeviceWaitIdle(g.device); | |
| vkDestroyBuffer(g.device, *buf, NULL); | |
| vkFreeMemory(g.device, *mem, NULL); | |
| } | |
| size_t nc = ((needed + (1<<20)-1) >> 20) << 20; | |
| allocDevBuf((VkDeviceSize)nc, usage, buf, mem); | |
| *cap = (*mem != VK_NULL_HANDLE) ? nc : 0; | |
| } | |
| /* One-shot: stage β device copy via command buffer (blocking) */ | |
| static void stageToDevice(VkBuffer dst, VkDeviceSize dstOff, | |
| VkDeviceSize srcOff, VkDeviceSize size) { | |
| VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; | |
| bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; | |
| vkBeginCommandBuffer(g.cmdBuf, &bi); | |
| VkBufferCopy cp = {srcOff, dstOff, size}; | |
| vkCmdCopyBuffer(g.cmdBuf, g.stageBuf, dst, 1, &cp); | |
| VkBufferMemoryBarrier bar = {VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; | |
| bar.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; | |
| bar.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; | |
| bar.srcQueueFamilyIndex = bar.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; | |
| bar.buffer = dst; bar.offset = dstOff; bar.size = size; | |
| vkCmdPipelineBarrier(g.cmdBuf, | |
| VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, | |
| 0,0,NULL,1,&bar,0,NULL); | |
| vkEndCommandBuffer(g.cmdBuf); | |
| VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO}; | |
| si.commandBufferCount = 1; si.pCommandBuffers = &g.cmdBuf; | |
| vkQueueSubmit(g.queue, 1, &si, g.fence); | |
| vkWaitForFences(g.device, 1, &g.fence, VK_TRUE, UINT64_MAX); | |
| vkResetFences(g.device, 1, &g.fence); | |
| } | |
| /* Upload large buffer to device-local in chunks β€ stageSize */ | |
| static int uploadChunked(VkBuffer dst, const float *src, size_t totalBytes) { | |
| size_t off = 0; | |
| while (off < totalBytes) { | |
| size_t chunk = totalBytes - off; | |
| if (chunk > g.stageSize) chunk = g.stageSize; | |
| memcpy(g.stageMapped, (const uint8_t*)src + off, chunk); | |
| stageToDevice(dst, (VkDeviceSize)off, 0, (VkDeviceSize)chunk); | |
| off += chunk; | |
| } | |
| return 0; | |
| } | |
| /* ββββββββββββββββββ Weight cache management ββββββββββββββββββ */ | |
| static WtEntry *cache_lookup(uint64_t key) { | |
| for (int i = 0; i < MAX_CACHE; i++) | |
| if (g_wt[i].valid && g_wt[i].key == key) return &g_wt[i]; | |
| return NULL; | |
| } | |
| static void cache_evict_lru(size_t needBytes) { | |
| while (g_wt_used_bytes + needBytes > g_wt_budget) { | |
| /* Find LRU entry */ | |
| int lru = -1; | |
| uint64_t lru_seq = UINT64_MAX; | |
| for (int i = 0; i < MAX_CACHE; i++) { | |
| if (g_wt[i].valid && g_wt[i].last_used < lru_seq) { | |
| lru_seq = g_wt[i].last_used; | |
| lru = i; | |
| } | |
| } | |
| if (lru < 0) break; | |
| vkDeviceWaitIdle(g.device); | |
| vkDestroyBuffer(g.device, g_wt[lru].buf, NULL); | |
| vkFreeMemory(g.device, g_wt[lru].mem, NULL); | |
| g_wt_used_bytes -= g_wt[lru].bytes; | |
| fprintf(stderr, "[cache] Evicted key=%016llx freed=%.1fMB used=%.0fMB\n", | |
| (unsigned long long)g_wt[lru].key, | |
| (double)g_wt[lru].bytes/1e6, | |
| (double)g_wt_used_bytes/1e6); | |
| g_wt[lru].valid = 0; | |
| g_evictions++; | |
| } | |
| } | |
| /* Insert weight B[K,N] into cache under key. Returns slot pointer or NULL. */ | |
| static WtEntry *cache_insert(uint64_t key, const float *B, | |
| uint32_t K, uint32_t N, size_t bytes) { | |
| /* Find free slot */ | |
| int slot = -1; | |
| for (int i = 0; i < MAX_CACHE; i++) { | |
| if (!g_wt[i].valid) { slot = i; break; } | |
| } | |
| if (slot < 0) { | |
| /* Evict LRU to make room */ | |
| uint64_t lru_seq = UINT64_MAX; int lru = 0; | |
| for (int i = 0; i < MAX_CACHE; i++) | |
| if (g_wt[i].valid && g_wt[i].last_used < lru_seq) | |
| { lru_seq = g_wt[i].last_used; lru = i; } | |
| vkDeviceWaitIdle(g.device); | |
| vkDestroyBuffer(g.device, g_wt[lru].buf, NULL); | |
| vkFreeMemory(g.device, g_wt[lru].mem, NULL); | |
| g_wt_used_bytes -= g_wt[lru].bytes; | |
| g_wt[lru].valid = 0; | |
| g_evictions++; | |
| slot = lru; | |
| } | |
| /* Ensure VRAM budget */ | |
| cache_evict_lru(bytes); | |
| /* Allocate device-local buffer */ | |
| WtEntry *e = &g_wt[slot]; | |
| allocDevBuf((VkDeviceSize)bytes, | |
| VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, | |
| &e->buf, &e->mem); | |
| if (e->mem == VK_NULL_HANDLE) return NULL; | |
| /* Upload weight (chunked if > stageSize) */ | |
| double t0 = now_ms(); | |
| uploadChunked(e->buf, B, bytes); | |
| double upload_ms = now_ms() - t0; | |
| e->key = key; | |
| e->bytes = bytes; | |
| e->K = K; | |
| e->N = N; | |
| e->last_used = ++g_wt_seq; | |
| e->valid = 1; | |
| g_wt_used_bytes += bytes; | |
| fprintf(stderr, "[cache] Stored key=%016llx [%uΓ%u] %.1fMB upload=%.1fms " | |
| "cache=%.0f/%.0fMB\n", | |
| (unsigned long long)key, K, N, | |
| (double)bytes/1e6, upload_ms, | |
| (double)g_wt_used_bytes/1e6, (double)g_wt_budget/1e6); | |
| return e; | |
| } | |
| /* ββββββββββββββββββ Descriptor & compute ββββββββββββββββββ */ | |
| static void updateDesc(VkBuffer bA, size_t szA, | |
| VkBuffer bB, size_t szB, | |
| VkBuffer bC, size_t szC) { | |
| VkDescriptorBufferInfo bi[3] = {}; | |
| bi[0].buffer=bA; bi[0].range=szA; | |
| bi[1].buffer=bB; bi[1].range=szB; | |
| bi[2].buffer=bC; bi[2].range=szC; | |
| VkWriteDescriptorSet wd[3] = {}; | |
| for (int i=0;i<3;i++) { | |
| wd[i].sType=VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; | |
| wd[i].dstSet=g.dSet; wd[i].dstBinding=(uint32_t)i; | |
| wd[i].descriptorCount=1; | |
| wd[i].descriptorType=VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; | |
| wd[i].pBufferInfo=&bi[i]; | |
| } | |
| vkUpdateDescriptorSets(g.device,3,wd,0,NULL); | |
| } | |
| /* Execute compute shader: dst=bufAΓbufB, shapes M,N,K */ | |
| static void runCompute(uint32_t M, uint32_t N, uint32_t K) { | |
| VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; | |
| bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; | |
| vkBeginCommandBuffer(g.cmdBuf, &bi); | |
| vkCmdBindPipeline(g.cmdBuf, VK_PIPELINE_BIND_POINT_COMPUTE, g.pipeline); | |
| vkCmdBindDescriptorSets(g.cmdBuf, VK_PIPELINE_BIND_POINT_COMPUTE, | |
| g.pipeLayout, 0,1,&g.dSet,0,NULL); | |
| uint32_t dims[3] = {M,N,K}; | |
| vkCmdPushConstants(g.cmdBuf, g.pipeLayout, | |
| VK_SHADER_STAGE_COMPUTE_BIT, 0, 12, dims); | |
| vkCmdDispatch(g.cmdBuf, (N+7)/8, (M+7)/8, 1); | |
| /* Barrier: compute β transfer (for download) */ | |
| VkBufferMemoryBarrier bar = {VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; | |
| bar.srcAccessMask=VK_ACCESS_SHADER_WRITE_BIT; | |
| bar.dstAccessMask=VK_ACCESS_TRANSFER_READ_BIT; | |
| bar.srcQueueFamilyIndex=bar.dstQueueFamilyIndex=VK_QUEUE_FAMILY_IGNORED; | |
| bar.buffer=g.bufC; bar.size=(VkDeviceSize)M*N*sizeof(float); | |
| vkCmdPipelineBarrier(g.cmdBuf, | |
| VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, | |
| 0,0,NULL,1,&bar,0,NULL); | |
| /* Download C */ | |
| VkBufferCopy cp = {0, 0, bar.size}; | |
| vkCmdCopyBuffer(g.cmdBuf, g.bufC, g.stageBuf, 1, &cp); | |
| vkEndCommandBuffer(g.cmdBuf); | |
| VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO}; | |
| si.commandBufferCount=1; si.pCommandBuffers=&g.cmdBuf; | |
| vkQueueSubmit(g.queue,1,&si,g.fence); | |
| vkWaitForFences(g.device,1,&g.fence,VK_TRUE,UINT64_MAX); | |
| vkResetFences(g.device,1,&g.fence); | |
| } | |
| /* ββββββββββββββββββ Vulkan init ββββββββββββββββββ */ | |
| static void vk_init(const char *spirvPath) { | |
| VkApplicationInfo appInfo = {VK_STRUCTURE_TYPE_APPLICATION_INFO}; | |
| appInfo.pApplicationName = "VkMatmulSrv"; appInfo.apiVersion = VK_API_VERSION_1_1; | |
| VkInstanceCreateInfo ici = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; | |
| ici.pApplicationInfo = &appInfo; | |
| if (vkCreateInstance(&ici,NULL,&g.instance) != VK_SUCCESS) die("vkCreateInstance"); | |
| uint32_t n=0; vkEnumeratePhysicalDevices(g.instance,&n,NULL); | |
| if (!n) die("No Vulkan devices"); | |
| VkPhysicalDevice *pd = (VkPhysicalDevice*)malloc(n*sizeof(*pd)); | |
| vkEnumeratePhysicalDevices(g.instance,&n,pd); | |
| g.phys = pd[0]; | |
| for (uint32_t i=0;i<n;i++) { | |
| VkPhysicalDeviceProperties p; vkGetPhysicalDeviceProperties(pd[i],&p); | |
| fprintf(stderr,"[vk] Device %u: %s\n",i,p.deviceName); | |
| if (p.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) g.phys = pd[i]; | |
| } | |
| free(pd); | |
| { VkPhysicalDeviceProperties p; vkGetPhysicalDeviceProperties(g.phys,&p); | |
| fprintf(stderr,"[vk] Selected: %s\n",p.deviceName); } | |
| /* Print available VRAM */ | |
| { VkPhysicalDeviceMemoryProperties mp; | |
| vkGetPhysicalDeviceMemoryProperties(g.phys,&mp); | |
| for (uint32_t i=0;i<mp.memoryHeapCount;i++) | |
| if (mp.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) | |
| fprintf(stderr,"[vk] VRAM heap %u: %.0f MB\n", | |
| i,(double)mp.memoryHeaps[i].size/1e6); } | |
| uint32_t nqf=0; vkGetPhysicalDeviceQueueFamilyProperties(g.phys,&nqf,NULL); | |
| VkQueueFamilyProperties *qp = (VkQueueFamilyProperties*)malloc(nqf*sizeof(*qp)); | |
| vkGetPhysicalDeviceQueueFamilyProperties(g.phys,&nqf,qp); | |
| g.queueFamily = UINT32_MAX; | |
| for (uint32_t i=0;i<nqf;i++) | |
| if (qp[i].queueFlags & VK_QUEUE_COMPUTE_BIT) { g.queueFamily=i; break; } | |
| free(qp); | |
| if (g.queueFamily==UINT32_MAX) die("No compute queue"); | |
| float prio=1.f; | |
| VkDeviceQueueCreateInfo qci = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; | |
| qci.queueFamilyIndex=g.queueFamily; qci.queueCount=1; qci.pQueuePriorities=&prio; | |
| VkDeviceCreateInfo dci = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; | |
| dci.queueCreateInfoCount=1; dci.pQueueCreateInfos=&qci; | |
| if (vkCreateDevice(g.phys,&dci,NULL,&g.device)!=VK_SUCCESS) die("vkCreateDevice"); | |
| vkGetDeviceQueue(g.device,g.queueFamily,0,&g.queue); | |
| /* Shader */ | |
| size_t sz; uint8_t *spv = load_file(spirvPath,&sz); | |
| if (!spv) die("load shader"); | |
| VkShaderModuleCreateInfo smci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; | |
| smci.codeSize=sz; smci.pCode=(const uint32_t*)spv; | |
| if (vkCreateShaderModule(g.device,&smci,NULL,&g.shaderModule)!=VK_SUCCESS) die("shader"); | |
| free(spv); | |
| fprintf(stderr,"[vk] Shader: %zu bytes SPIR-V\n",sz); | |
| /* Descriptor layout: 3 storage buffers */ | |
| VkDescriptorSetLayoutBinding binds[3]={}; | |
| for(int i=0;i<3;i++){binds[i].binding=(uint32_t)i;binds[i].descriptorType=VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; | |
| binds[i].descriptorCount=1;binds[i].stageFlags=VK_SHADER_STAGE_COMPUTE_BIT;} | |
| VkDescriptorSetLayoutCreateInfo dsli={VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; | |
| dsli.bindingCount=3; dsli.pBindings=binds; | |
| vkCreateDescriptorSetLayout(g.device,&dsli,NULL,&g.dsetLayout); | |
| VkPushConstantRange pcr={VK_SHADER_STAGE_COMPUTE_BIT,0,12}; | |
| VkPipelineLayoutCreateInfo pli={VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; | |
| pli.setLayoutCount=1; pli.pSetLayouts=&g.dsetLayout; | |
| pli.pushConstantRangeCount=1; pli.pPushConstantRanges=&pcr; | |
| vkCreatePipelineLayout(g.device,&pli,NULL,&g.pipeLayout); | |
| VkPipelineShaderStageCreateInfo ss={VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; | |
| ss.stage=VK_SHADER_STAGE_COMPUTE_BIT; ss.module=g.shaderModule; ss.pName="main"; | |
| VkComputePipelineCreateInfo cpci={VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; | |
| cpci.stage=ss; cpci.layout=g.pipeLayout; | |
| if (vkCreateComputePipelines(g.device,VK_NULL_HANDLE,1,&cpci,NULL,&g.pipeline)!=VK_SUCCESS) | |
| die("compute pipeline"); | |
| VkCommandPoolCreateInfo cpi={VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; | |
| cpi.queueFamilyIndex=g.queueFamily; | |
| cpi.flags=VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; | |
| vkCreateCommandPool(g.device,&cpi,NULL,&g.cmdPool); | |
| VkCommandBufferAllocateInfo cbai={VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; | |
| cbai.commandPool=g.cmdPool; cbai.level=VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount=1; | |
| vkAllocateCommandBuffers(g.device,&cbai,&g.cmdBuf); | |
| VkDescriptorPoolSize dps={VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,3}; | |
| VkDescriptorPoolCreateInfo dpci={VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; | |
| dpci.maxSets=1; dpci.poolSizeCount=1; dpci.pPoolSizes=&dps; | |
| vkCreateDescriptorPool(g.device,&dpci,NULL,&g.dPool); | |
| VkDescriptorSetAllocateInfo dsai={VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; | |
| dsai.descriptorPool=g.dPool; dsai.descriptorSetCount=1; dsai.pSetLayouts=&g.dsetLayout; | |
| vkAllocateDescriptorSets(g.device,&dsai,&g.dSet); | |
| VkFenceCreateInfo fi={VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; | |
| vkCreateFence(g.device,&fi,NULL,&g.fence); | |
| /* Staging buffer */ | |
| g.stageSize = (size_t)STAGING_MB*1024*1024; | |
| VkBufferCreateInfo sbi={VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; | |
| sbi.size=g.stageSize; sbi.sharingMode=VK_SHARING_MODE_EXCLUSIVE; | |
| sbi.usage=VK_BUFFER_USAGE_TRANSFER_SRC_BIT|VK_BUFFER_USAGE_TRANSFER_DST_BIT; | |
| vkCreateBuffer(g.device,&sbi,NULL,&g.stageBuf); | |
| VkMemoryRequirements smr; vkGetBufferMemoryRequirements(g.device,g.stageBuf,&smr); | |
| VkMemoryAllocateInfo smai={VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; | |
| smai.allocationSize=smr.size; | |
| smai.memoryTypeIndex=findMemType(smr.memoryTypeBits, | |
| VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT|VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); | |
| if (vkAllocateMemory(g.device,&smai,NULL,&g.stageMem)!=VK_SUCCESS) | |
| die("staging alloc (BAR too small?)"); | |
| vkBindBufferMemory(g.device,g.stageBuf,g.stageMem,0); | |
| vkMapMemory(g.device,g.stageMem,0,g.stageSize,0,&g.stageMapped); | |
| g.capA = g.capC = 0; | |
| g.bufA = g.bufC = VK_NULL_HANDLE; | |
| g.memA = g.memC = VK_NULL_HANDLE; | |
| memset(g_wt, 0, sizeof(g_wt)); | |
| fprintf(stderr,"[vk] Init OK β staging=%zuMB wt_cache_budget=%zuMB\n", | |
| g.stageSize>>20, g_wt_budget>>20); | |
| } | |
| /* ββββββββββββββββββ Matmul implementations ββββββββββββββββββ */ | |
| /* | |
| * do_matmul_with_buffers: run shader with pre-populated bufA, bufB_dev, bufC | |
| * A must already be in g.bufA (uploaded via staging) | |
| * B must be in bufB_dev (device-local, cached or just uploaded) | |
| */ | |
| static int run_matmul(uint32_t M, uint32_t N, uint32_t K, | |
| VkBuffer bufB_dev, size_t szB, float *C_out) { | |
| size_t szA = (size_t)M*K*sizeof(float); | |
| size_t szC = (size_t)M*N*sizeof(float); | |
| ensureDevBuf(szC, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, &g.bufC, &g.memC, &g.capC); | |
| updateDesc(g.bufA, szA, bufB_dev, szB, g.bufC, szC); | |
| runCompute(M, N, K); | |
| /* runCompute downloads C into staging[0:szC] */ | |
| memcpy(C_out, g.stageMapped, szC); | |
| return 0; | |
| } | |
| /* Upload A from socket into g.bufA */ | |
| static int upload_A(int fd, uint32_t M, uint32_t K) { | |
| size_t szA = (size_t)M*K*sizeof(float); | |
| ensureDevBuf(szA, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, &g.bufA, &g.memA, &g.capA); | |
| /* Receive A directly into staging, then copy to device */ | |
| size_t off = 0; | |
| while (off < szA) { | |
| size_t chunk = szA - off; | |
| if (chunk > g.stageSize) chunk = g.stageSize; | |
| if (recv_all(fd, g.stageMapped, chunk) != 0) return -1; | |
| /* Stage β bufA */ | |
| VkCommandBufferBeginInfo bi={VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; | |
| bi.flags=VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; | |
| vkBeginCommandBuffer(g.cmdBuf,&bi); | |
| VkBufferCopy cp={0,(VkDeviceSize)off,(VkDeviceSize)chunk}; | |
| vkCmdCopyBuffer(g.cmdBuf,g.stageBuf,g.bufA,1,&cp); | |
| VkBufferMemoryBarrier bar={VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; | |
| bar.srcAccessMask=VK_ACCESS_TRANSFER_WRITE_BIT; | |
| bar.dstAccessMask=VK_ACCESS_SHADER_READ_BIT; | |
| bar.srcQueueFamilyIndex=bar.dstQueueFamilyIndex=VK_QUEUE_FAMILY_IGNORED; | |
| bar.buffer=g.bufA; bar.offset=(VkDeviceSize)off; bar.size=(VkDeviceSize)chunk; | |
| vkCmdPipelineBarrier(g.cmdBuf, | |
| VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, | |
| 0,0,NULL,1,&bar,0,NULL); | |
| vkEndCommandBuffer(g.cmdBuf); | |
| VkSubmitInfo si={VK_STRUCTURE_TYPE_SUBMIT_INFO}; | |
| si.commandBufferCount=1; si.pCommandBuffers=&g.cmdBuf; | |
| vkQueueSubmit(g.queue,1,&si,g.fence); | |
| vkWaitForFences(g.device,1,&g.fence,VK_TRUE,UINT64_MAX); | |
| vkResetFences(g.device,1,&g.fence); | |
| off += chunk; | |
| } | |
| return 0; | |
| } | |
| /* Upload B from socket into a new/existing cache entry */ | |
| static WtEntry *upload_B_and_cache(int fd, uint64_t key, uint32_t K, uint32_t N) { | |
| size_t szB = (size_t)K*N*sizeof(float); | |
| float *Btmp = (float*)malloc(szB); | |
| if (!Btmp) return NULL; | |
| if (recv_all(fd, Btmp, szB) != 0) { free(Btmp); return NULL; } | |
| WtEntry *e = cache_insert(key, Btmp, K, N, szB); | |
| free(Btmp); | |
| return e; | |
| } | |
| /* βββββββββββββββββββ Fast single-submit path ββββββββββββββββββ */ | |
| /* | |
| * compute_cached_fast(): | |
| * Used when the weight is already in VRAM (a_only=1) AND | |
| * szA + szC β€ stageSize (always true for generation tokens). | |
| * | |
| * ONE command buffer: | |
| * stageBuf[0:szA] β bufA (upload A from socket) | |
| * dispatch shader (A Γ B_cached β C) | |
| * bufC β stageBuf[szA:szA+szC] (download C) | |
| * ONE vkQueueSubmit + ONE vkWaitForFences | |
| * | |
| * Eliminates the second fence-wait of the v2 slow path (~43ms β ~22ms). | |
| * | |
| * Falls back to slow path (upload_A + run_matmul, 2 submits) if the | |
| * activation + result don't fit in a single staging pass. | |
| */ | |
| static int compute_cached_fast(int fd, uint32_t M, uint32_t N, uint32_t K, | |
| WtEntry *e, float *C_out) { | |
| size_t szA = (size_t)M * K * sizeof(float); | |
| size_t szC = (size_t)M * N * sizeof(float); | |
| /* Fast path: A + C fit in staging together */ | |
| if (szA + szC <= g.stageSize) { | |
| double t_recv = now_ms(); | |
| /* Receive A directly into staging[0:szA] */ | |
| if (recv_all(fd, g.stageMapped, szA) != 0) return -1; | |
| double recv_ms = now_ms() - t_recv; | |
| /* Ensure device-local A and C buffers are large enough */ | |
| ensureDevBuf(szA, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, &g.bufA, &g.memA, &g.capA); | |
| ensureDevBuf(szC, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, &g.bufC, &g.memC, &g.capC); | |
| /* Update descriptors: A=bufA, B=e->buf (cached weight), C=bufC */ | |
| updateDesc(g.bufA, szA, e->buf, e->bytes, g.bufC, szC); | |
| double t_gpu = now_ms(); | |
| /* Build ONE command buffer: copy A β dispatch β copy C */ | |
| VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; | |
| bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; | |
| vkBeginCommandBuffer(g.cmdBuf, &bi); | |
| /* 1) staging[0:szA] β bufA */ | |
| VkBufferCopy cpA = {0, 0, (VkDeviceSize)szA}; | |
| vkCmdCopyBuffer(g.cmdBuf, g.stageBuf, g.bufA, 1, &cpA); | |
| /* Barrier: transfer write β shader read on bufA */ | |
| VkBufferMemoryBarrier barA = {VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; | |
| barA.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; | |
| barA.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; | |
| barA.srcQueueFamilyIndex = barA.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; | |
| barA.buffer = g.bufA; barA.size = (VkDeviceSize)szA; | |
| vkCmdPipelineBarrier(g.cmdBuf, | |
| VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, | |
| 0, 0, NULL, 1, &barA, 0, NULL); | |
| /* 2) Dispatch compute shader */ | |
| vkCmdBindPipeline(g.cmdBuf, VK_PIPELINE_BIND_POINT_COMPUTE, g.pipeline); | |
| vkCmdBindDescriptorSets(g.cmdBuf, VK_PIPELINE_BIND_POINT_COMPUTE, | |
| g.pipeLayout, 0, 1, &g.dSet, 0, NULL); | |
| uint32_t dims[3] = {M, N, K}; | |
| vkCmdPushConstants(g.cmdBuf, g.pipeLayout, | |
| VK_SHADER_STAGE_COMPUTE_BIT, 0, 12, dims); | |
| vkCmdDispatch(g.cmdBuf, (N + 7) / 8, (M + 7) / 8, 1); | |
| /* Barrier: shader write β transfer read on bufC */ | |
| VkBufferMemoryBarrier barC = {VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; | |
| barC.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; | |
| barC.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; | |
| barC.srcQueueFamilyIndex = barC.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; | |
| barC.buffer = g.bufC; barC.size = (VkDeviceSize)szC; | |
| vkCmdPipelineBarrier(g.cmdBuf, | |
| VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, | |
| 0, 0, NULL, 1, &barC, 0, NULL); | |
| /* 3) bufC β staging[szA:szA+szC] */ | |
| VkBufferCopy cpC = {0, (VkDeviceSize)szA, (VkDeviceSize)szC}; | |
| vkCmdCopyBuffer(g.cmdBuf, g.bufC, g.stageBuf, 1, &cpC); | |
| vkEndCommandBuffer(g.cmdBuf); | |
| /* ONE submit, ONE fence wait (vs two in slow path) */ | |
| VkSubmitInfo si = {VK_STRUCTURE_TYPE_SUBMIT_INFO}; | |
| si.commandBufferCount = 1; si.pCommandBuffers = &g.cmdBuf; | |
| vkQueueSubmit(g.queue, 1, &si, g.fence); | |
| vkWaitForFences(g.device, 1, &g.fence, VK_TRUE, UINT64_MAX); | |
| vkResetFences(g.device, 1, &g.fence); | |
| double gpu_ms = now_ms() - t_gpu; | |
| /* Copy result out of staging */ | |
| memcpy(C_out, (uint8_t *)g.stageMapped + szA, szC); | |
| fprintf(stderr, "[fast] [%uΓ%uΓ%u] recv=%.1fms gpu=%.1fms total=%.1fms\n", | |
| M, N, K, recv_ms, gpu_ms, recv_ms + gpu_ms); | |
| return 0; | |
| } | |
| /* Slow fallback: szA+szC > stageSize (very large prefill batch) */ | |
| if (upload_A(fd, M, K) != 0) return -1; | |
| return run_matmul(M, N, K, e->buf, e->bytes, C_out); | |
| } | |
| /* ββββββββββββββββββ Client handler ββββββββββββββββββ */ | |
| typedef struct __attribute__((packed)) { | |
| uint32_t magic; | |
| uint32_t M, N, K; | |
| uint32_t type; /* 0=plain, 1=cached */ | |
| uint32_t a_only; /* 1=don't send B (expect cache hit) */ | |
| uint32_t key_lo; | |
| uint32_t key_hi; | |
| } ReqHdr; /* 32 bytes */ | |
| typedef struct __attribute__((packed)) { | |
| uint32_t magic; | |
| uint32_t status; | |
| uint32_t M, N; | |
| } RspHdr; /* 16 bytes */ | |
| static void send_rsp(int fd, uint32_t status, uint32_t M, uint32_t N) { | |
| RspHdr r = {MAGIC_VK, status, M, N}; | |
| send_all(fd, &r, sizeof(r)); | |
| } | |
| static void handle_client(int fd) { | |
| fprintf(stderr,"[server] Client connected fd=%d\n",fd); | |
| uint64_t req_n = 0; | |
| for (;;) { | |
| ReqHdr req; | |
| if (recv_all(fd, &req, sizeof(req)) != 0) break; | |
| if (req.magic != MAGIC_VK) { fprintf(stderr,"[server] bad magic\n"); break; } | |
| uint32_t M=req.M, N=req.N, K=req.K; | |
| uint64_t key = ((uint64_t)req.key_hi << 32) | req.key_lo; | |
| req_n++; | |
| double t0 = now_ms(); | |
| size_t szC = (size_t)M*N*sizeof(float); | |
| float *C = (float*)malloc(szC); | |
| if (!C) { send_rsp(fd,3,0,0); break; } | |
| if (req.type == TYPE_PLAIN) { | |
| /* ββ Plain matmul: receive A+B, compute, return C ββ */ | |
| if (upload_A(fd, M, K) != 0) { free(C); break; } | |
| size_t szB = (size_t)K*N*sizeof(float); | |
| float *Btmp = (float*)malloc(szB); | |
| if (!Btmp) { free(C); send_rsp(fd,3,0,0); break; } | |
| if (recv_all(fd, Btmp, szB) != 0) { free(Btmp); free(C); break; } | |
| /* Allocate temp device buffer for B */ | |
| VkBuffer tmpBuf; VkDeviceMemory tmpMem; | |
| allocDevBuf((VkDeviceSize)szB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, &tmpBuf, &tmpMem); | |
| uploadChunked(tmpBuf, Btmp, szB); | |
| free(Btmp); | |
| run_matmul(M, N, K, tmpBuf, szB, C); | |
| vkDeviceWaitIdle(g.device); | |
| vkDestroyBuffer(g.device, tmpBuf, NULL); | |
| vkFreeMemory(g.device, tmpMem, NULL); | |
| send_rsp(fd, STATUS_OK, M, N); | |
| send_all(fd, C, szC); | |
| } else { | |
| /* ββ Cached matmul ββ */ | |
| WtEntry *e = cache_lookup(key); | |
| if (req.a_only && !e) { | |
| /* Client thought weight was cached, but it was evicted */ | |
| fprintf(stderr,"[cache] STALE key=%016llx β NEED_WEIGHT\n", | |
| (unsigned long long)key); | |
| send_rsp(fd, STATUS_NEED_W, M, N); | |
| free(C); | |
| g_cache_misses++; | |
| continue; | |
| } | |
| if (!e) { | |
| /* ββ Cache miss: upload A + B, run, cache B ββ */ | |
| if (upload_A(fd, M, K) != 0) { free(C); break; } | |
| e = upload_B_and_cache(fd, key, K, N); | |
| if (!e) { free(C); send_rsp(fd,3,0,0); break; } | |
| g_cache_misses++; | |
| /* Two-submit slow path (first time for this weight) */ | |
| if (run_matmul(M, N, K, e->buf, e->bytes, C) != 0) { free(C); break; } | |
| } else if (req.a_only) { | |
| /* ββ Cache hit, a_only=1: FAST single-submit path ββ */ | |
| e->last_used = ++g_wt_seq; | |
| g_cache_hits++; | |
| if (compute_cached_fast(fd, M, N, K, e, C) != 0) { free(C); break; } | |
| } else { | |
| /* ββ Cache hit, a_only=0: client sent both A and B. | |
| * This path should rarely trigger when using _next_key() | |
| * monotonic IDs, but we keep it as a safety net for any | |
| * client that still uses address-based keys. | |
| * Upload A normally, drain B (discard it β weight is valid | |
| * in cache), then run matmul with two submits. ββ */ | |
| if (upload_A(fd, M, K) != 0) { free(C); break; } | |
| { | |
| size_t drain = (size_t)K * N * sizeof(float); | |
| size_t done = 0; | |
| int ok = 1; | |
| while (done < drain && ok) { | |
| size_t chunk = drain - done; | |
| if (chunk > g.stageSize) chunk = g.stageSize; | |
| if (recv_all(fd, g.stageMapped, chunk) != 0) ok = 0; | |
| done += chunk; | |
| } | |
| if (!ok) { free(C); break; } | |
| fprintf(stderr, "[cache] key=%016llx hit(a_only=0): drained %.0fMB\n", | |
| (unsigned long long)key, (double)drain/1e6); | |
| } | |
| e->last_used = ++g_wt_seq; | |
| g_cache_hits++; | |
| if (run_matmul(M, N, K, e->buf, e->bytes, C) != 0) { free(C); break; } | |
| } | |
| send_rsp(fd, STATUS_OK, M, N); | |
| send_all(fd, C, szC); | |
| } | |
| free(C); | |
| double elapsed = now_ms()-t0; | |
| /* Periodic stats */ | |
| if (req_n % 50 == 0) { | |
| fprintf(stderr,"[server] req#%llu hits=%llu miss=%llu evict=%llu " | |
| "cache=%.0f/%.0fMB\n", | |
| (unsigned long long)req_n, | |
| (unsigned long long)g_cache_hits, | |
| (unsigned long long)g_cache_misses, | |
| (unsigned long long)g_evictions, | |
| (double)g_wt_used_bytes/1e6, | |
| (double)g_wt_budget/1e6); | |
| } | |
| (void)elapsed; | |
| } | |
| close(fd); | |
| fprintf(stderr,"[server] Client disconnected after %llu reqs " | |
| "hits=%llu miss=%llu cache=%.0fMB used\n", | |
| (unsigned long long)req_n, | |
| (unsigned long long)g_cache_hits, | |
| (unsigned long long)g_cache_misses, | |
| (double)g_wt_used_bytes/1e6); | |
| } | |
| /* ββββββββββββββββββ main ββββββββββββββββββ */ | |
| int main(int argc, char **argv) { | |
| int port = 8097; | |
| const char *spirvPath = "matmul.spv"; | |
| if (argc > 1) port = atoi(argv[1]); | |
| if (argc > 2) spirvPath = argv[2]; | |
| const char *cache_mb = getenv("VK_CACHE_MB"); | |
| if (cache_mb) g_wt_budget = (size_t)atoi(cache_mb)*1024*1024; | |
| fprintf(stderr,"=== Vulkan Matmul Server v3 (Single-Submit Fast Path) ===\n"); | |
| fprintf(stderr,"Port=%d SPIR-V=%s cache_budget=%zuMB\n", | |
| port, spirvPath, g_wt_budget>>20); | |
| vk_init(spirvPath); | |
| int srv = socket(AF_INET,SOCK_STREAM,0); if(srv<0) die("socket"); | |
| int opt=1; setsockopt(srv,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); | |
| struct sockaddr_in addr={}; | |
| addr.sin_family=AF_INET; addr.sin_addr.s_addr=INADDR_ANY; | |
| addr.sin_port=htons((uint16_t)port); | |
| if (bind(srv,(struct sockaddr*)&addr,sizeof(addr))<0) die("bind"); | |
| if (listen(srv,4)<0) die("listen"); | |
| fprintf(stderr,"[server] Listening on :%d\n",port); | |
| for (;;) { | |
| struct sockaddr_in cli; socklen_t cl=sizeof(cli); | |
| int fd = accept(srv,(struct sockaddr*)&cli,&cl); | |
| if (fd<0) { perror("accept"); continue; } | |
| handle_client(fd); | |
| } | |
| } | |