row_store.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. // Exact immutable FP8 row retrieval; bounded set-associative RAM cache.
  2. // No CUDA calls in the callback: suitable for cudaLaunchHostFunc graph nodes.
  3. //
  4. // The callback blocks the compute stream, so every microsecond spent here is a
  5. // microsecond the GPU is idle. Two things keep it short:
  6. // * misses are serviced by a shared thread pool (NVMe at QD=1 delivers ~3.5k
  7. // IOPS on GB10, at QD=64 ~112k -- the drive, not the latency, is the limit);
  8. // * a miss costs one pread, not two into unrelated 4 KiB pages -- either from
  9. // a repacked local shard (weight+scale adjacent, and no NFS hop on a worker)
  10. // or, failing that, from the owned scale shard pinned in RAM (8 B/row).
  11. #include <algorithm>
  12. #include <atomic>
  13. #include <cerrno>
  14. #include <condition_variable>
  15. #include <cstdint>
  16. #include <cstdio>
  17. #include <cstdlib>
  18. #include <cstring>
  19. #include <fcntl.h>
  20. #include <mutex>
  21. #include <sys/mman.h>
  22. #ifdef DSV41_ROW_STORE_DRM
  23. #include <sys/ioctl.h>
  24. #include <drm/drm.h>
  25. #include <drm/drm_mode.h>
  26. #endif
  27. #include <sys/stat.h>
  28. #include <thread>
  29. #include <unistd.h>
  30. #include <vector>
  31. namespace {
  32. constexpr uint64_t kRowBytes = 264; // 256 B weight + 8 B scale
  33. constexpr uint64_t kWeightBytes = 256;
  34. constexpr uint64_t kScaleBytes = 8;
  35. constexpr uint64_t kPage = 4096;
  36. constexpr uint64_t kPackedHeader = 4096; // keeps row 0 page-aligned
  37. constexpr uint64_t kPackedMagic = 0x31344e4531565344ULL; // "DSV41EN1"
  38. // One id per ticket. Misses inside a ticket are serviced serially by the thread
  39. // that claimed it, so a larger chunk would cap in-flight reads at count/kChunk --
  40. // exactly the queue depth this pool exists to raise. A decode step at batch 1
  41. // only offers ~144 ids per layer, and fetch_add is ~20 ns against a ~200 us read.
  42. constexpr uint64_t kChunk = 1;
  43. constexpr uint64_t kSerialBelow = 8; // dispatch is not worth it under this
  44. uint64_t env_u64(const char *name, uint64_t fallback) {
  45. const char *raw = std::getenv(name);
  46. if (!raw || !*raw) return fallback;
  47. char *end = nullptr;
  48. const unsigned long long value = std::strtoull(raw, &end, 10);
  49. if (end == raw) return fallback;
  50. return uint64_t(value);
  51. }
  52. bool env_flag(const char *name, bool fallback) {
  53. const char *raw = std::getenv(name);
  54. if (!raw || !*raw) return fallback;
  55. return !(std::strcmp(raw, "0") == 0 || std::strcmp(raw, "off") == 0 ||
  56. std::strcmp(raw, "false") == 0);
  57. }
  58. } // namespace
  59. struct Store {
  60. int fd;
  61. uint64_t rows, weight_offset, scale_offset, slots, row_lo, row_hi;
  62. uint64_t sets = 0, ways = 1;
  63. uint8_t *cache = nullptr;
  64. uint64_t *keys = nullptr;
  65. uint8_t *victim = nullptr; // per-set round-robin replacement cursor
  66. uint8_t *resident = nullptr; // OFFLOAD_MODE=ram: whole shard mapped
  67. size_t resident_size = 0;
  68. uint8_t *scales = nullptr; // owned scale rows, [row_lo, row_hi)
  69. size_t scales_size = 0;
  70. int packed_fd = -1; // repacked owned rows, weight+scale adjacent
  71. std::mutex locks[256];
  72. std::atomic<uint64_t> hits{0}, misses{0}, reads{0};
  73. };
  74. struct Work {
  75. Store *store;
  76. const int64_t *ids;
  77. uint8_t *weights, *scales;
  78. uint64_t count;
  79. };
  80. static void fail(const char *reason) {
  81. std::fprintf(stderr, "Engram retrieval failed: %s (errno=%d)\n", reason, errno);
  82. std::abort(); // Never allow a generation to continue with missing/stale rows.
  83. }
  84. static void read_from(Store *s, int fd, uint64_t offset, uint8_t *out, size_t length) {
  85. if (s->resident && fd == s->fd) {
  86. std::memcpy(out, s->resident + offset, length);
  87. return;
  88. }
  89. alignas(4096) uint8_t page[8192];
  90. const uint64_t base = offset & ~(kPage - 1);
  91. const size_t delta = offset - base;
  92. const size_t requested = ((delta + length + kPage - 1) / kPage) * kPage;
  93. ssize_t got;
  94. do { got = pread(fd, page, requested, base); } while (got < 0 && errno == EINTR);
  95. if (got < 0 || size_t(got) < delta + length) fail("short or failed direct read");
  96. std::memcpy(out, page + delta, length);
  97. s->reads.fetch_add(1, std::memory_order_relaxed);
  98. }
  99. static void read_bytes(Store *s, uint64_t offset, uint8_t *out, size_t length) {
  100. read_from(s, s->fd, offset, out, length);
  101. }
  102. // Fetch one owned row into `row` (264 B), consulting and filling the cache.
  103. static void fetch_row(Store *s, uint64_t id, uint8_t *row) {
  104. const uint64_t set = s->sets ? id % s->sets : 0;
  105. std::mutex &lock = s->locks[set % 256];
  106. if (s->sets) {
  107. std::lock_guard<std::mutex> guard(lock);
  108. const uint64_t base = set * s->ways;
  109. for (uint64_t w = 0; w < s->ways; ++w) {
  110. if (s->keys[base + w] == id + 1) {
  111. std::memcpy(row, s->cache + (base + w) * kRowBytes, kRowBytes);
  112. s->hits.fetch_add(1, std::memory_order_relaxed);
  113. return;
  114. }
  115. }
  116. }
  117. // Miss: read outside the lock so same-set misses overlap on the drive.
  118. if (s->packed_fd >= 0) {
  119. // Weight and scale are adjacent here, so the whole row is one read -- and on
  120. // a worker it is a local read instead of an NFS round trip to the head.
  121. read_from(s, s->packed_fd, kPackedHeader + (id - s->row_lo) * kRowBytes,
  122. row, kRowBytes);
  123. } else {
  124. read_bytes(s, s->weight_offset + id * kWeightBytes, row, kWeightBytes);
  125. if (s->scales) {
  126. std::memcpy(row + kWeightBytes, s->scales + (id - s->row_lo) * kScaleBytes,
  127. kScaleBytes);
  128. } else {
  129. read_bytes(s, s->scale_offset + id * kScaleBytes, row + kWeightBytes, kScaleBytes);
  130. }
  131. }
  132. s->misses.fetch_add(1, std::memory_order_relaxed);
  133. if (s->sets) {
  134. std::lock_guard<std::mutex> guard(lock);
  135. const uint64_t base = set * s->ways;
  136. const uint64_t w = s->victim[set] % s->ways;
  137. s->victim[set] = uint8_t((s->victim[set] + 1) % s->ways);
  138. std::memcpy(s->cache + (base + w) * kRowBytes, row, kRowBytes);
  139. s->keys[base + w] = id + 1;
  140. }
  141. }
  142. static void serve(Work *work, uint64_t begin, uint64_t end) {
  143. Store *s = work->store;
  144. for (uint64_t i = begin; i < end; ++i) {
  145. const int64_t id = work->ids[i];
  146. if (id < 0 || uint64_t(id) >= s->rows) fail("row ID out of bounds");
  147. if (uint64_t(id) < s->row_lo || uint64_t(id) >= s->row_hi) {
  148. std::memset(work->weights + i * kWeightBytes, 0, kWeightBytes);
  149. std::memset(work->scales + i * kScaleBytes, 0, kScaleBytes);
  150. continue;
  151. }
  152. uint8_t row[kRowBytes];
  153. fetch_row(s, uint64_t(id), row);
  154. std::memcpy(work->weights + i * kWeightBytes, row, kWeightBytes);
  155. std::memcpy(work->scales + i * kScaleBytes, row + kWeightBytes, kScaleBytes);
  156. }
  157. }
  158. namespace {
  159. // One process-wide pool. The two Engram layers never look up concurrently, so
  160. // they share it rather than each holding its own idle threads.
  161. class Pool {
  162. public:
  163. explicit Pool(uint64_t threads) {
  164. workers_.reserve(threads);
  165. for (uint64_t i = 0; i < threads; ++i) workers_.emplace_back([this] { loop(); });
  166. }
  167. uint64_t size() const { return workers_.size(); }
  168. void run(Work *work) {
  169. // One job at a time: the two Engram layers issue their callbacks on the same
  170. // stream, but a shared pool must not depend on that.
  171. std::lock_guard<std::mutex> serialize(caller_);
  172. const uint64_t chunks = (work->count + kChunk - 1) / kChunk;
  173. {
  174. std::lock_guard<std::mutex> guard(mutex_);
  175. job_ = work;
  176. next_.store(0, std::memory_order_relaxed);
  177. chunks_ = chunks;
  178. remaining_.store(chunks, std::memory_order_relaxed);
  179. ++generation_;
  180. }
  181. ready_.notify_all();
  182. drain(work); // the caller works too, so progress never depends on a wakeup
  183. // Wait for the chunks *and* for every worker to leave drain(): only then is
  184. // it safe for the next job to reset the ticket counter under them.
  185. std::unique_lock<std::mutex> guard(mutex_);
  186. done_.wait(guard, [this] {
  187. return remaining_.load(std::memory_order_acquire) == 0 &&
  188. active_.load(std::memory_order_acquire) == 0;
  189. });
  190. job_ = nullptr;
  191. }
  192. ~Pool() {
  193. {
  194. std::lock_guard<std::mutex> guard(mutex_);
  195. stop_ = true;
  196. ++generation_;
  197. }
  198. ready_.notify_all();
  199. for (auto &worker : workers_) worker.join();
  200. }
  201. private:
  202. void drain(Work *work) {
  203. if (!work) return;
  204. for (;;) {
  205. const uint64_t ticket = next_.fetch_add(1, std::memory_order_relaxed);
  206. if (ticket >= chunks_) return;
  207. const uint64_t begin = ticket * kChunk;
  208. serve(work, begin, std::min(begin + kChunk, work->count));
  209. if (remaining_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
  210. std::lock_guard<std::mutex> guard(mutex_);
  211. done_.notify_one();
  212. }
  213. }
  214. }
  215. void loop() {
  216. uint64_t seen = 0;
  217. for (;;) {
  218. Work *work = nullptr;
  219. {
  220. std::unique_lock<std::mutex> guard(mutex_);
  221. ready_.wait(guard, [this, seen] { return stop_ || generation_ != seen; });
  222. if (stop_) return;
  223. seen = generation_;
  224. work = job_;
  225. // Counted under the lock so run()'s predicate can never miss a worker
  226. // that is about to enter drain().
  227. if (work) active_.fetch_add(1, std::memory_order_release);
  228. }
  229. if (!work) continue;
  230. drain(work);
  231. std::lock_guard<std::mutex> guard(mutex_);
  232. active_.fetch_sub(1, std::memory_order_release);
  233. done_.notify_one();
  234. }
  235. }
  236. std::vector<std::thread> workers_;
  237. std::mutex mutex_, caller_;
  238. std::condition_variable ready_, done_;
  239. Work *job_ = nullptr;
  240. std::atomic<uint64_t> next_{0}, remaining_{0}, active_{0};
  241. uint64_t chunks_ = 0, generation_ = 0;
  242. bool stop_ = false;
  243. };
  244. Pool *pool() {
  245. static Pool *instance = [] {
  246. const uint64_t threads = env_u64("DSV41_IO_THREADS", 32);
  247. return threads > 1 ? new Pool(threads - 1) : nullptr; // caller is a worker
  248. }();
  249. return instance;
  250. }
  251. // Pin the owned scale rows so a miss costs one pread, not two.
  252. bool pin_scales(Store *s) {
  253. const uint64_t count = s->row_hi - s->row_lo;
  254. if (!count) return false;
  255. const uint64_t bytes = count * kScaleBytes;
  256. void *map = mmap(nullptr, bytes, PROT_READ | PROT_WRITE,
  257. MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  258. if (map == MAP_FAILED) return false;
  259. auto *out = static_cast<uint8_t *>(map);
  260. const uint64_t start = s->scale_offset + s->row_lo * kScaleBytes;
  261. const uint64_t end = start + bytes;
  262. if (s->resident) {
  263. std::memcpy(out, s->resident + start, bytes);
  264. s->scales = out;
  265. s->scales_size = bytes;
  266. return true;
  267. }
  268. const size_t span = size_t(8) << 20;
  269. void *raw = nullptr;
  270. if (posix_memalign(&raw, kPage, span)) { munmap(map, bytes); return false; }
  271. auto *buffer = static_cast<uint8_t *>(raw);
  272. uint64_t offset = start & ~(kPage - 1);
  273. while (offset < end) {
  274. const uint64_t want = std::min<uint64_t>(span, ((end - offset + kPage - 1) / kPage) * kPage);
  275. ssize_t got;
  276. do { got = pread(s->fd, buffer, size_t(want), off_t(offset)); }
  277. while (got < 0 && errno == EINTR);
  278. if (got <= 0) { std::free(raw); munmap(map, bytes); return false; }
  279. const uint64_t lo = std::max(offset, start);
  280. const uint64_t hi = std::min<uint64_t>(offset + uint64_t(got), end);
  281. if (hi > lo) std::memcpy(out + (lo - start), buffer + (lo - offset), hi - lo);
  282. offset += uint64_t(got);
  283. }
  284. std::free(raw);
  285. s->scales = out;
  286. s->scales_size = bytes;
  287. return true;
  288. }
  289. } // namespace
  290. #ifdef DSV41_ROW_STORE_DRM
  291. // Optional backing for the next store's row cache: a DRM dumb buffer carved from the GB10
  292. // firmware's display reservation (needs nvidia_drm modeset=1 fbdev=0, no display server). That
  293. // memory is outside MemAvailable, so the cache stops competing with the KV pool. Only the CPU
  294. // touches the cache (rows are staged to the GPU through pinned buffers); CPU reads from this
  295. // mapping are uncached (~1 GB/s), which a few hundred 264-byte rows per step absorb.
  296. static char g_drm_node[256];
  297. static uint64_t g_drm_bytes = 0;
  298. extern "C" void row_store_next_cache_on_drm(const char *node, uint64_t bytes) {
  299. std::snprintf(g_drm_node, sizeof g_drm_node, "%s", node ? node : "");
  300. g_drm_bytes = bytes;
  301. }
  302. static uint8_t *drm_cache(uint64_t need) {
  303. const uint64_t pitch = 4096 * 4;
  304. uint64_t bytes = g_drm_bytes;
  305. g_drm_bytes = 0; // one store only
  306. if (!bytes || !g_drm_node[0] || need > bytes || bytes % (pitch * 1024)) return nullptr;
  307. int fd = open(g_drm_node, O_RDWR | O_CLOEXEC);
  308. if (fd < 0) { std::fprintf(stderr, "Engram: open %s failed (errno %d)\n", g_drm_node, errno); return nullptr; }
  309. struct drm_get_cap cap = {};
  310. cap.capability = DRM_CAP_DUMB_BUFFER;
  311. struct drm_mode_create_dumb req = {};
  312. req.width = 4096; req.height = uint32_t(bytes / pitch); req.bpp = 32;
  313. if (ioctl(fd, DRM_IOCTL_GET_CAP, &cap) || !cap.value || ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &req) ||
  314. req.size != bytes) {
  315. std::fprintf(stderr, "Engram: no dumb buffer of %llu bytes on %s (errno %d)\n",
  316. (unsigned long long)bytes, g_drm_node, errno);
  317. close(fd); return nullptr;
  318. }
  319. struct drm_mode_map_dumb map = {};
  320. map.handle = req.handle;
  321. void *p = MAP_FAILED;
  322. if (!ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map))
  323. p = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, map.offset);
  324. if (p == MAP_FAILED) { close(fd); return nullptr; }
  325. std::fprintf(stderr, "Engram: row cache of %llu bytes on the display reservation (%s)\n",
  326. (unsigned long long)need, g_drm_node);
  327. return static_cast<uint8_t *>(p); // fd and mapping live as long as the process
  328. }
  329. #endif
  330. extern "C" Store *row_store_open(const char *path, uint64_t rows,
  331. uint64_t woff, uint64_t soff, uint64_t budget) {
  332. auto *s = new Store;
  333. const char *mode = std::getenv("OFFLOAD_MODE");
  334. const bool ram = mode && std::strcmp(mode, "ram") == 0;
  335. s->fd = open(path, O_RDONLY | O_CLOEXEC | (ram ? 0 : O_DIRECT));
  336. if (s->fd < 0 && !ram) {
  337. // SSHFS / NFS often reject O_DIRECT; buffered reads still return exact bytes.
  338. s->fd = open(path, O_RDONLY | O_CLOEXEC);
  339. if (s->fd >= 0) {
  340. std::fprintf(stderr,
  341. "Engram: O_DIRECT unavailable for %s, using buffered reads (errno was %d)\n",
  342. path, errno);
  343. }
  344. }
  345. if (s->fd < 0) { delete s; return nullptr; }
  346. struct stat statbuf;
  347. if (fstat(s->fd, &statbuf) || woff > uint64_t(statbuf.st_size) ||
  348. soff > uint64_t(statbuf.st_size) || rows > (uint64_t(statbuf.st_size)-woff)/256 ||
  349. rows > (uint64_t(statbuf.st_size)-soff)/8) fail("invalid table extent");
  350. if (ram) {
  351. s->resident_size = statbuf.st_size;
  352. s->resident = static_cast<uint8_t *>(mmap(nullptr,s->resident_size,
  353. PROT_READ,MAP_SHARED | MAP_POPULATE,s->fd,0));
  354. if (s->resident == MAP_FAILED) fail("resident mapping");
  355. if (mlock(s->resident,s->resident_size)) fail("RAM mode requires memlock capability and sufficient RAM");
  356. budget = 0;
  357. }
  358. s->rows = rows; s->weight_offset = woff; s->scale_offset = soff;
  359. s->row_lo = 0; s->row_hi = rows;
  360. s->ways = std::max<uint64_t>(1, std::min<uint64_t>(16, env_u64("DSV41_CACHE_WAYS", 4)));
  361. s->sets = budget / ((kRowBytes + sizeof(uint64_t)) * s->ways + 1);
  362. s->slots = s->sets * s->ways;
  363. if (s->slots) {
  364. #ifdef DSV41_ROW_STORE_DRM
  365. s->cache = drm_cache(s->slots * kRowBytes);
  366. if (!s->cache)
  367. #endif
  368. s->cache = static_cast<uint8_t *>(mmap(nullptr, s->slots * kRowBytes,
  369. PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
  370. s->keys = static_cast<uint64_t *>(mmap(nullptr, s->slots * sizeof(uint64_t),
  371. PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
  372. s->victim = static_cast<uint8_t *>(mmap(nullptr, s->sets,
  373. PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
  374. if (s->cache == MAP_FAILED || s->keys == MAP_FAILED || s->victim == MAP_FAILED)
  375. fail("cache allocation");
  376. }
  377. return s;
  378. }
  379. extern "C" void row_store_lookup(void *opaque) {
  380. auto *work = static_cast<Work *>(opaque);
  381. Pool *workers = pool();
  382. if (!workers || work->count < kSerialBelow) {
  383. serve(work, 0, work->count);
  384. return;
  385. }
  386. workers->run(work);
  387. }
  388. extern "C" void row_store_stats(Store *s, uint64_t *out) {
  389. out[0] = s->hits.load(); out[1] = s->misses.load(); out[2] = s->reads.load();
  390. out[3] = s->slots * (kRowBytes + sizeof(uint64_t)) + s->sets;
  391. out[4] = s->slots;
  392. out[5] = s->scales_size;
  393. out[6] = s->ways;
  394. out[8] = s->packed_fd >= 0 ? 1 : 0;
  395. Pool *workers = pool();
  396. out[7] = workers ? workers->size() + 1 : 1;
  397. }
  398. // Attach a shard produced by scripts/pack_engram.py: this rank's owned rows,
  399. // weight and scale adjacent, on local disk. Missing is fine (we fall back to the
  400. // checkpoint); present but wrong is a repack bug and must never be served.
  401. extern "C" int row_store_attach_packed(Store *s, const char *path, uint64_t layer_id) {
  402. const int fd = open(path, O_RDONLY | O_CLOEXEC | O_DIRECT);
  403. if (fd < 0) return 0;
  404. alignas(4096) uint8_t header[kPage];
  405. ssize_t got;
  406. do { got = pread(fd, header, kPage, 0); } while (got < 0 && errno == EINTR);
  407. if (got < ssize_t(kPage)) { close(fd); fail("packed Engram shard has no header"); }
  408. uint64_t magic, lo, hi, rows, layer, row_bytes;
  409. std::memcpy(&magic, header + 0, 8);
  410. std::memcpy(&layer, header + 8, 8);
  411. std::memcpy(&lo, header + 16, 8);
  412. std::memcpy(&hi, header + 24, 8);
  413. std::memcpy(&rows, header + 32, 8);
  414. std::memcpy(&row_bytes, header + 40, 8);
  415. if (magic != kPackedMagic || layer != layer_id || lo != s->row_lo ||
  416. hi != s->row_hi || rows != s->rows || row_bytes != kRowBytes)
  417. fail("packed Engram shard does not match this rank's range; re-run the pack step");
  418. struct stat statbuf;
  419. if (fstat(fd, &statbuf) ||
  420. uint64_t(statbuf.st_size) < kPackedHeader + (hi - lo) * kRowBytes)
  421. fail("packed Engram shard is truncated");
  422. s->packed_fd = fd;
  423. return 1;
  424. }
  425. extern "C" void row_store_range(Store *s, uint64_t lo, uint64_t hi) {
  426. if (lo > hi || hi > s->rows) fail("invalid row ownership range");
  427. // The pinned shard is indexed off row_lo, so moving the range under it would
  428. // silently return another row's scale. Rows are immutable and the range is set
  429. // once at load; anything else is a bug, not a case to paper over.
  430. if (s->scales && (lo != s->row_lo || hi != s->row_hi))
  431. fail("ownership range changed after the scale shard was pinned");
  432. s->row_lo = lo; s->row_hi = hi;
  433. if (!s->scales && hi > lo && env_flag("DSV41_RESIDENT_SCALES", true) && !pin_scales(s)) {
  434. std::fprintf(stderr, "Engram: could not pin the owned scale shard (%llu rows); "
  435. "misses will cost a second read\n",
  436. static_cast<unsigned long long>(hi - lo));
  437. }
  438. }
  439. extern "C" void row_store_close(Store *s) {
  440. if (s->packed_fd >= 0) close(s->packed_fd);
  441. if (s->resident) munmap(s->resident,s->resident_size);
  442. if (s->scales) munmap(s->scales, s->scales_size);
  443. if (s->slots) {
  444. munmap(s->cache, s->slots * kRowBytes);
  445. munmap(s->keys, s->slots * sizeof(uint64_t));
  446. munmap(s->victim, s->sets);
  447. }
  448. close(s->fd); delete s;
  449. }