tables-overview

Table 实现全览

概述

Table 是 NV Embedding Cache SDK 中所有存储后端的统一抽象基类,定义了以下纯虚接口:

class Table {
  virtual ~Table() = default;
  virtual void clear(context_ptr_t& ctx) = 0;
  virtual void erase(context_ptr_t& ctx, int64_t n, const void* keys) = 0;
  virtual void find(context_ptr_t& ctx, int64_t n, const void* keys,
                    max_bitmask_repr_t* hit_mask, int64_t value_stride,
                    void* values, int64_t* value_sizes) const = 0;
  virtual void insert(context_ptr_t& ctx, int64_t n, const void* keys,
                      int64_t value_stride, int64_t value_size, const void* values) = 0;
  virtual void update(...) = 0;
  virtual void update_accumulate(...) = 0;
  virtual context_ptr_t create_execution_context(...) = 0;
  virtual void reset_lookup_counter(...) = 0;
  virtual void get_lookup_counter(...) = 0;
  virtual bool lookup_counter_hits() = 0;
  virtual int32_t get_device_id() const = 0;
  virtual int64_t get_max_row_size() const = 0;
  virtual int64_t get_invalid_key() const = 0;
};

项目实现了 8 种表,按存储位置和底层技术可分为:

类别 表实现 存储位置 数据持久化 依赖
GPU 缓存表 GpuTable GPU 显存 cuEmbed + EmbedCacheSA
CPU 线性表 LinearHostTable CPU 内存(用户分配) 无(memcpy 直接读写)
CPU 哈希表(内置) STLContainerTable CPU 内存 std::unordered_map
CPU 哈希表(Abseil) AbseilFlatMapTable CPU 内存 Abseil Swiss Table
CPU 哈希表(PHMAP) PHMapFlatMapTable CPU 内存 Parallel Hashmap
CPU GPU 哈希表 NvhmMapTable CPU 内存(nvhashmap) NVIDIA nvhashmap
远程缓存 RedisClusterTable 远程 Redis 集群 ✅ 持久化 hiredis + redis++
磁盘存储 RocksDBTable 本地磁盘 ✅ 持久化 Facebook RocksDB

一、Table 基类接口详解

核心操作契约

find(ctx, n, keys, hit_mask, value_stride, values, value_sizes)

  • 输入hit_mask 中 bit=0 的键需要被查找,bit=1 的键应跳过
  • 输出:对于 bit=0 且在表中找到的键,将其 bit 设为 1,并将值写入 values 的对应位置
  • 紧凑写入:如果 values 是 nullptr,则只做计数/键存在性检查

insert(ctx, n, keys, value_stride, value_size, values)

  • 向表中插入键值对。如果键已存在,行为取决于实现(替换或忽略)
  • 最佳努力操作:如果触发了超额淘汰,新插入的键可能立即被淘汰

update(ctx, n, keys, value_stride, value_size, values)

  • 只覆盖表中已存在的键,不存在的键被忽略
  • GpuTable 如果配置了 uvm_table 且未禁用 UVM 更新,会同时写入 UVM 后备表

update_accumulate(ctx, n, keys, update_stride, update_size, updates, update_dtype)

  • 梯度累积模式:table[key] += updates[i]
  • update_dtype 可以不同于表的存储类型(如 fp32 梯度累积到 fp16 表)

命中率计数

方法 作用
reset_lookup_counter(ctx) 重置计数器
get_lookup_counter(ctx, counter) 读取计数器值(GPU 表需先 cudaStreamSynchronize
lookup_counter_hits() false = 计数的是 miss,true = 计数的是 hit

二、GpuTable — GPU 集合关联缓存表

头文件include/gpu_table.hpp | 实现src/gpu_table.cu

GpuTable 是 NVE 的核心 GPU 端表实现,内部封装了 EmbedCacheSA<KeyType, KeyType> ——一个集合关联(Set-Associative)软件管理缓存

架构层次

GpuTable
  └── EmbedCacheSA<KeyType, KeyType>  (GPU 集合关联缓存)
        ├── EmbedCacheSA 基类: 标签匹配 + 数据区 + 替换策略
        ├── CacheSADeviceModify: 修改操作在 GPU 上执行
        └── CacheSAHostModify: 修改操作在 CPU 上执行(调试用途)

查找的四种 kernel 模式

uvm_table 非空时,run_find_uvm() 根据键数量选择执行策略:

Kernel 模式 阈值条件 行为
LookupUVM 默认:num_keys < 1M 一次 cache->lookup 调用,cache miss 时直接从 UVM table 读取
SortGather 默认:num_keys ≥ 1M 先排序键(去重),大块连续读取 UVM,改善 page fault 局部性
PipelineGather 用户明确指定 流水线 gather kernel,分 tile 处理
DynamicKernel 暂未实现

Insert 的直方图优化

GpuTable 的 insert 操作使用 DefaultGPUHistogram 在 GPU 上对键做频率统计和去重

输入: [1, 2, 1, 3, 2, 1, 1]  (7 个键)
  ↓
DefaultGPUHistogram(GPU kernel)
  ↓
输出: 键 [1, 2, 3] 按频率排序 (4, 2, 1)
  ↓
cache->insert() → 高频键优先插入 cache

频率越高的键在 insert 中获得越高优先级,在集合关联缓存中更不容易被淘汰。


三、LinearHostTable — CPU 线性表

头文件include/linear_host_table.hpp | 实现src/linear_host_table.cpp

设计理念

LinearHostTable 是最简单的 CPU 端表实现:嵌入向量存储在用户提供的连续内存缓冲区中,通过 key * row_size 直接计算地址后 memcpy 读写。无哈希、无索引、无淘汰——这与 GPUEmbeddingLayer 在 GPU 端的角色完全对称。

实现

// 查找: 最简单的 memcpy
void find(...) {
    thread_pool->execute_n(0, num_tasks, [=](int64_t idx) {
        for each key 跳过 hitmask 已标记的:
            memcpy(output + k * output_stride,
                   src + keys[k] * row_size_in_bytes,
                   row_size_in_bytes);
    });
    memset(hit_mask, 0xff, ...);
}

适用场景

  • GpuTable 配合,UVM 后备表的 CPU 端视图
  • 纯 CPU 推理场景,嵌入表可完全装入系统内存
  • 作为 HierarchicalEmbeddingLayer 的中间层

四、STLContainerTable — 内置 CPU 哈希表

头文件include/stl_map_backed_table.hpp | 实现src/stl_map_backed_table.cpp

架构

STLContainerTable 是所有基于 STL 风格容器的 CPU 哈希表实现的公共基类。它使用 std::unordered_map<KeyType, char*> 作为底层映射,核心价值在于它实现了一套完整的内存管理、并发控制、超额淘汰基础设施。

STLContainerTable  (模板基类)
  ├── 持有 std::vector<Partition>
  │     └── Partition:
  │           ├── std::shared_mutex  (读写锁)
  │           ├── std::unordered_map<KeyType, char*>  (键 → 槽指针)
  │           └── SlotBuffer (手动管理的内存池)
  │
  ├── 查找: 分片并发 + hitmask 过滤 + 软件预取
  ├── 插入: 分片加锁 + 槽分配 + 超额淘汰
  ├── 更新: 分片加锁 + 直接覆盖
  ├── 累积: 分片加锁 + 原子累加 + inline meta
  └── 淘汰: 三种策略 → EvictRandom / EvictLRU / EvictLFU

分片与并发

所有操作按 partitioner(key) & (num_partitions - 1) 分配到对应分片:

  • 查找:每个分片加共享锁(shared_lock),多线程并行
  • 修改:每个分片加独占锁(unique_lock),保证写一致性

内存槽管理

STLContainerTable 不把值直接存在 std::unordered_map 的 value 中,而是手动管理一个连续的 slot buffer:

Slot Buffer 布局:
┌────────────────────────────────────────────────┐
│ Row 0  │  value_bytes  │  meta_bytes  │ padding│
│ Row 1  │  value_bytes  │  meta_bytes  │ padding│
│ ...... │  .............│ .............│ .......│
│ Row N  │  value_bytes  │  meta_bytes  │ padding│
└────────────────────────────────────────────────┘
  • value_bytes:嵌入向量数据
  • meta_bytes:inline metadata(LRU 时间戳或 LFU 频率计数)
  • std::unordered_map 只存 key → char*(指向 slot 起始位置)

这种设计将哈希表索引和数据存储分离,元数据跟随数据一起移动,淘汰时只需排序 meta 即可决定谁被淘汰。

超额淘汰(Overflow)

当插入导致分片容量超过阈值时触发淘汰:

overflow_margin = 512(额外条目数)
resolution_margin = 0.2(淘汰比例 20%)

触发条件: partition.size() > initial_capacity + overflow_margin
淘汰过程:
  1. 收集所有 key + meta
  2. 按 meta 排序:
     - EvictRandom: 随机打乱
     - EvictLRU: 按时间戳排序(最久未使用优先)
     - EvictLFU: 按频率排序(最少使用优先)
  3. 淘汰 resolution_margin 比例的条目
  4. 释放对应的 slot 内存

模板分发

produce() 方法按 mask_sizekey_sizeoverflow_handlerpartitioner 四维 switch 分发,生成约 192 种编译时类型特化。

内置别名

系统中预注册了 6 个别名指向同一个基于 std::unordered_map 的 STLMapTableFactory:

{"stl_map", "map", "stl_unordered_map", "unordered_map", "stl_umap", "umap"}

五、AbseilFlatMapTable — Abseil Swiss Table 插件

头文件plugins/abseil/include/abseil_flat_map_table.hpp | 注册名abseil_flat_map

继承 STLContainerTable,将底层容器替换为 absl::flat_hash_map<KeyType, char*>absl::flat_hash_map 使用 Google 的 Swiss Table 算法(开放寻址 + 元数据字节 + SIMD 查找),比 std::unordered_map 有更好的内存局部性和查找性能。

维度 STLContainerTable AbseilFlatMapTable
底层容器 std::unordered_map(链式哈希) absl::flat_hash_map(Swiss Table)
额外配置 initial_capacity 用于 reserve()
链接依赖 absl_raw_logging_internal + absl_hash

六、PHMapFlatMapTable — Parallel Hashmap 插件

头文件plugins/phmap/include/phmap_flat_map_table.hpp | 注册名phmap_flat_map

实现方式与 Abseil 插件完全相同——继承 STLContainerTable,底层替换为 phmap::flat_hash_map<KeyType, char*>。PHMAP 使用与 Abseil 类似的开放寻址算法,但实现更轻量,无外部链接依赖(header-only)。


七、NvhmMapTable — NVIDIA nvhashmap 插件

头文件plugins/nvhm/include/nvhm_map_table.hpp | 注册名nvhm_map

与 STLContainerTable 的区别

NvhmMapTable 不继承 STLContainerTable,而是直接继承 HostTable<NvhmMapTableConfig>,直接使用 NVIDIA nvhashmap 的 map 接口:

template <typename MaskType, typename MapType, typename PartitionerType>
class NvhmMapTable final : public HostTable<NvhmMapTableConfig> {
    struct Partition {
        mutable std::shared_mutex read_write;
        map_type map;  // nvhm::map<KeyType, ValueType, ...>
    };
    std::vector<Partition> parts_;
};

配置

字段 默认 说明
kernel_size nvhm 默认 内核大小 11024(对应 81024B entry)
key_fetch_queue_length 8 软件预取管道深度 {0,1,2,4,8}
prefetch_values true 是否发出值预取指令
minimize_psl false 缩短 probe search length
auto_shrink false 大量淘汰后自动缩容

查找中的软件预取

nvhashmap 的 ring_prefetch_queue 实现了软件流水线预取

for (int i = 0; i < KeyFetchQueueLength; i++) {
    __builtin_prefetch(next_key_address);
}

find_() 模板方法中,通过 KeyFetchQueueLengthPrefetchValues 两个编译时参数控制预取行为,switch 生成 5 × 2 = 10 种变体。

模板分发

NvhmMapTable 的模板分发维度最多:

mask_size × key_size × overflow_handler × kernel_size × minimize_psl × auto_shrink × partitioner

约 6144 种编译时类型组合。


八、RedisClusterTable — Redis 远程表

头文件plugins/redis/include/redis_cluster_table.hpp | 注册名redis_cluster

架构

RedisClusterTable 是 NVE 中唯一基于网络通信的表实现。它通过 hiredis(C 客户端)和 redis++(C++ 客户端)连接到外部 Redis Cluster,将嵌入向量以序列化形式存储在 Redis 中。

配置

字段 默认 说明
address localhost:6379 Redis 集群任意节点地址
user_name default 用户名
password 密码
connections_per_node 5 每节点最大并行连接数
connection_lifetime 180s 连接生命周期
max_batch_size 16384 批量操作最大大小
num_partitions 1 并行分片数(2 的幂)

查找实现

auto reply = redis_cluster_->pipeline(keys.begin(), keys.end(),
    [](const auto& key) { return sw::redis::command("GET", key); });

对于每个分片,使用 redis++ 的 pipeline 功能批量发送 GET 命令,减少网络往返次数。

链接依赖

hiredis + hiredis_ssl + redis++_static + ssl + crypto


九、RocksDBTable — 磁盘持久化表

头文件plugins/rocksdb/include/rocksdb_table.hpp | 注册名rocksdb

架构

RocksDBTable 通过 Facebook RocksDB 提供本地磁盘持久化存储。嵌入向量以 key → value 形式存储在 RocksDB 的 Column Family 中。

配置

字段 默认 说明
column_family default RocksDB Column Family
max_batch_size 16384 批量操作最大大小
verify_checksums true 是否校验 checksum
path /tmp/rocksdb 数据库文件路径
read_only false 只读模式(允许多进程并发读取)
num_threads 16 后台线程数

查找实现

db_->MultiGet(read_opts_, col_families_, keys_slices, &values, &statuses);

使用 RocksDB 的 MultiGet 实现批量读取,单次调用返回多个键的状态。


十、所有 Table 的对比总表

维度 GpuTable LinearHostTable STLContainer AbseilFlatMap PHMapFlatMap NvhmMapTable RedisCluster RocksDB
存储位置 GPU 显存 CPU 线性内存 CPU 堆内存 CPU 堆内存 CPU 堆内存 CPU 堆内存 远程集群 本地磁盘
数据持久化
查找算法 集合关联 tag key * stride unordered_map O(1) Swiss Table O(1) PHMAP O(1) nvhashmap O(1) Redis GET LSM-Tree
并发粒控 CUDA stream 线程池并行 分片读写锁 分片读写锁 分片读写锁 分片读写锁 pipeline + 连接池 RocksDB 内部
淘汰策略 集合关联 LRU 无(固定数组) EvictRandom/LRU/LFU 同左 同左 同左 无(Redis 自管) 无(磁盘存储)
是否插件 核心库内置 核心库内置 核心库内置 ✅ abseil 插件 ✅ phmap 插件 ✅ nvhm 插件 ✅ redis 插件 ✅ rocksdb 插件
启动依赖 CUDA + nvhashmap absl PHMAP header-only nvhashmap hiredis + redis++ librocksdb
JSON 实现名 stl_map/umap abseil_flat_map phmap_flat_map nvhm_map redis_cluster rocksdb
典型扮演角色 层级首表 层级中/末表 层级中层 层级中层 层级中层 层级中层 层级末表 层级末表

十一、在嵌入层中的角色总结

HierarchicalEmbeddingLayer:
  tables[0] ─── GpuTable (GPU cache, 最快层)
  tables[1] ─── NvhmMapTable / AbseilFlatMapTable / STLContainerTable (CPU hash, 中间层)
  tables[2] ─── RedisClusterTable / RocksDBTable (远程/持久化, 末层)

LinearUVMEmbeddingLayer:
  GpuTable.cache_ = EmbedCacheSA (GPU cache)
  GpuTable.config_.uvm_table = UVM 线性内存 (硬件透明回退)

GPUEmbeddingLayer:
  无 Table (直接 cuEmbed Forward kernel, 线性 GPU 内存)