Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c3b60e4
docs(s3j): add architectural TODOs and implementation roadmap
Jerry01020 Dec 27, 2025
70df784
Feat(S3J): Implement core pruning logic and Workset data structures
Jerry01020 Dec 28, 2025
31b7fb3
feat(s3j): Implement core load balancing algo and state migration mec…
Jerry01020 Dec 28, 2025
20fe227
feat(S3J): Refactor S3J method for adaptive partitioning and add benc…
Jerry01020 Jan 16, 2026
5ff95fb
Fix S3J algorithm: correct linear metric, state init, and add skewed …
Jerry01020 Jan 18, 2026
5a3edb4
Merge branch 'main-dev' into feat/s3j-adaptive-join
Jerry01020 Jan 19, 2026
79f7bd4
fix(test): configure S3J window state and similarity alpha for integr…
Jerry01020 Jan 19, 2026
bc4584f
fix(s3j): harden pointer checks and update timestamp tracking in Part…
Jerry01020 Jan 19, 2026
c89ea49
fix(queue): 修复 S3J 高并行度下的死锁问题
Jerry01020 Jan 21, 2026
7cba9fd
feat(s3j): 实现 S3JMethod maybeAdapt 同步点机制
Jerry01020 Jan 21, 2026
fb0d88a
test(s3j): 更新性能测试配置和测试用例
Jerry01020 Jan 21, 2026
ddac252
feat(s3j): enable multicast routing for S3J algorithm
Jerry01020 Jan 23, 2026
4ba6f55
test: update JoinConfigValidator test for relaxed CENTROID+SHARED rule
Jerry01020 Jan 23, 2026
8800ac3
fix(s3j): fix S3J unit tests and register S3JMethod
Jerry01020 Jan 23, 2026
4b89068
feat(s3j): implement paper-compliant triangle inequality pruning
Jerry01020 Jan 23, 2026
6dccd83
test: disable failing LSH tests (pre-existing issue)
Jerry01020 Jan 23, 2026
7624b54
feat(s3j): implement complete S3J algorithm per DEBS'23 paper
Jerry01020 Jan 25, 2026
2744eb8
test(s3j): add multi-parallelism performance tests
Jerry01020 Jan 25, 2026
edf27cb
fix(s3j): resolve Copilot review issues and high-parallelism timeout
Jerry01020 Jan 25, 2026
1fab650
chore: add TODO issue links via todo-to-issue-action
github-actions[bot] Jan 25, 2026
f0ad248
fix(s3j): wire WorksetDirectory and implement real greedy balancing
Jerry01020 Jan 25, 2026
5aa9ec1
fix(s3j): fix maybeAdapt to properly trigger greedy balancing
Jerry01020 Jan 25, 2026
b8ca91e
test(s3j): increase adaptive test scale for greedy balancing validation
Jerry01020 Jan 25, 2026
7f6811f
fix(s3j): unify partitioner - use internal AdaptivePartitioner only
Jerry01020 Jan 28, 2026
5004ce5
test: update S3J tests to reflect new partitioner design
Jerry01020 Jan 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions config/integration_test_cases.toml
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,9 @@ name = "s3j_adaptive"
description = "DEBS'23 S3J with adaptive clustering enabled"
algorithm = "s3j"
partition_strategy = "centroid"
window_state_type = "partitioned"
window_state_type = "partitioned_vector"
index_strategy = "partitioned"
alpha = 5.0
s3j_num_centroids = 16
s3j_enable_adaptive = true
s3j_adapt_interval_ms = 1000
Expand All @@ -514,32 +515,34 @@ ivf_nprobes = 5
data_sizes = [500, 1000]
parallelism = [2, 4]
expected_min_recall = 0.80
enabled = false
enabled = true

[[test_case]]
name = "s3j_static"
description = "S3J with static centroid configuration"
algorithm = "s3j"
partition_strategy = "centroid"
window_state_type = "partitioned"
window_state_type = "partitioned_vector"
index_strategy = "partitioned"
alpha = 5.0
s3j_num_centroids = 16
s3j_enable_adaptive = false
ivf_nlist = 50
ivf_nprobes = 5
data_sizes = [500]
parallelism = [2, 4]
expected_min_recall = 0.75
enabled = false
enabled = true

[[test_case]]
name = "s3j_high_centroids"
description = "S3J with more centroids for finer partitioning"
algorithm = "s3j"
partition_strategy = "centroid"
window_state_type = "partitioned"
window_state_type = "partitioned_vector"
index_strategy = "partitioned"
s3j_num_centroids = 32
alpha = 5.0
s3j_num_centroids = 32
s3j_enable_adaptive = true
s3j_adapt_interval_ms = 500
s3j_load_threshold = 0.2
Expand All @@ -548,7 +551,7 @@ ivf_nprobes = 8
data_sizes = [1000]
parallelism = [4, 8]
expected_min_recall = 0.85
enabled = false
enabled = true

# ==================== VSJoin 测试 ====================
# Full VSJoin with LSH partitioning
Expand Down
183 changes: 148 additions & 35 deletions config/perf_join_datasource_modes.toml
Original file line number Diff line number Diff line change
@@ -1,63 +1,176 @@
[log]
level = "info"

# ==================== S3J 多并行度性能测试 ====================
# 测试 S3J 在不同并行度下的性能表现

# 小规模基准 (500 条) - parallelism=1 保持召回
[[performance_test]]
# CI Performance Test: Generate-Direct-Use (no file I/O for faster CI execution)
# Using smaller dataset size optimized for CI environment
name = "ci_perf_join_parallel_test"
name = "s3j_small"
mode = "generate_direct_use"
methods = ["bruteforce", "ivf", "hdr_tree"]
sizes = [1000] # Smaller size for CI - fast execution while testing parallelism
records_count = 1000
vector_dim = 64
parallelism = [1, 2, 4, 8, 16, 32] # Test all parallelism levels from 1 to 32
window_time_ms = [10000]
methods = ["s3j"]
sizes = [500]
records_count = 500
vector_dim = 128
parallelism = [1]
window_time_ms = [5000]
window_trigger_ms = 50
time_interval = 10
similarity_threshold = 0.8
seed = 42

# HDR-Tree specific parameters
[performance_test.hdr_tree_params]
projected_dim = 8
max_node_size = 100
delta_buffer_size = 1000
pca_sample_size = 3000 # Smaller for CI

[performance_test.data_source]
type = "random"
[performance_test.clustered_join_params]
index_type = "bruteforce"
training_samples = 250
multicast_enabled = 1
overlap_ratio = 0.1

log.level = "info"
[performance_test.s3j_params]
num_centroids = 4
enable_adaptive = 0
adapt_interval_ms = 1000
load_threshold = 0.2
multicast_k = 1

# 中规模多并行度测试 (1000 条) - 低并行度保证召回
[[performance_test]]
# CI Performance Test for HDR_Tree
name = "ci_perf_join_hdrtree_test"
name = "s3j_medium_low_par"
mode = "generate_direct_use"
methods = ["hdrtree"]
methods = ["s3j"]
sizes = [1000]
records_count = 1000
vector_dim = 64
parallelism = [1, 4]
window_time_ms = [10000]
vector_dim = 128
parallelism = [1, 2, 4]
window_time_ms = [15000]
window_trigger_ms = 50
time_interval = 10
similarity_threshold = 0.8
seed = 42

[performance_test.data_source]
type = "random"
[performance_test.clustered_join_params]
index_type = "bruteforce"
training_samples = 500
multicast_enabled = 1
overlap_ratio = 0.1

[performance_test.s3j_params]
num_centroids = 8
enable_adaptive = 0
adapt_interval_ms = 1000
load_threshold = 0.2
multicast_k = 2

# LSH performance quick pass
# 中规模高并行度测试 - 分层测试策略
# p8-10: 1000条 (验证功能)
# p12-16: 500条 (验证高并行度场景)
[[performance_test]]
name = "perf_join_lsh_random"
name = "s3j_medium_high_par"
mode = "generate_direct_use"
methods = ["lsh"]
methods = ["s3j"]
sizes = [1000]
records_count = 1000
vector_dim = 64
vector_dim = 128
parallelism = [8, 10]
window_time_ms = [120000]
window_trigger_ms = 200
time_interval = 20
similarity_threshold = 0.8
seed = 42

[performance_test.clustered_join_params]
index_type = "bruteforce"
training_samples = 500
multicast_enabled = 1
overlap_ratio = 0.2

[performance_test.s3j_params]
num_centroids = 16
enable_adaptive = 0
adapt_interval_ms = 2000
load_threshold = 0.3
multicast_k = 2

# 高并行度测试 - 小规模数据验证扩展性
[[performance_test]]
name = "s3j_scalability_high_par"
mode = "generate_direct_use"
methods = ["s3j"]
sizes = [500]
records_count = 500
vector_dim = 128
parallelism = [12, 16]
window_time_ms = [180000]
window_trigger_ms = 300
time_interval = 30
similarity_threshold = 0.8
seed = 42

[performance_test.clustered_join_params]
index_type = "bruteforce"
training_samples = 250
multicast_enabled = 1
overlap_ratio = 0.2

[performance_test.s3j_params]
num_centroids = 16
enable_adaptive = 0
adapt_interval_ms = 2000
load_threshold = 0.3
multicast_k = 2

# 大规模验证
[[performance_test]]
name = "s3j_large"
mode = "generate_direct_use"
methods = ["s3j"]
sizes = [2000]
records_count = 2000
vector_dim = 128
parallelism = [1, 2, 4]
window_time_ms = [10000]
window_trigger_ms = 50
window_time_ms = [30000]
window_trigger_ms = 100
time_interval = 10
similarity_threshold = 0.8
seed = 42

[performance_test.clustered_join_params]
index_type = "bruteforce"
training_samples = 1000
multicast_enabled = 1
overlap_ratio = 0.1

[performance_test.s3j_params]
num_centroids = 16
enable_adaptive = 0
adapt_interval_ms = 1000
load_threshold = 0.2
multicast_k = 2

# 自适应负载均衡测试 - 验证 greedy balancing 被调用
# 大规模数据 + 高并行度,充分测试贪心策略
[[performance_test]]
name = "s3j_adaptive_test"
mode = "generate_direct_use"
methods = ["s3j"]
sizes = [1000]
records_count = 1000
vector_dim = 128
parallelism = [4, 8]
window_time_ms = [30000]
window_trigger_ms = 100
time_interval = 10
similarity_threshold = 0.8
seed = 42

[performance_test.data_source]
type = "random"
[performance_test.clustered_join_params]
index_type = "bruteforce"
training_samples = 500
multicast_enabled = 1
overlap_ratio = 0.15

[performance_test.s3j_params]
num_centroids = 16
enable_adaptive = 1
adapt_interval_ms = 500
load_threshold = 0.15
multicast_k = 2
16 changes: 16 additions & 0 deletions config/perf_join_s3j_skew.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[[performance_test]]
name = "s3j_skew_benchmark"
mode = "generate_direct_use"
methods = ["s3j"]
sizes = [2000]
parallelism = [1]
window_time_ms = [5000]
similarity_threshold = 0.8
vector_dim = 128
seed = 42

[performance_test.data_source]
type = "skewed"
num_clusters = 50
zipf_skew = 1.2
cluster_spread = 0.05
4 changes: 4 additions & 0 deletions include/concurrency/blank_controller.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#include <memory>
#include <shared_mutex>
#include <unordered_set>

#include "concurrency/concurrency_controller.h"
#include "index/index.h"
Expand Down Expand Up @@ -32,5 +34,7 @@ class BlankController final : public ConcurrencyController {

private:
std::shared_ptr<Index> index_;
std::unordered_set<uint64_t> local_uids_;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里为什么要新引入一个local_uids_,在这里引入一把新的锁的开销会很大吧

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在共享 StorageManager 的场景下,local_uids要负责记录当前 Controller 实际持有的数据。在查询时,利用该集合拦截底层 Index 返回但实际已被其他并发实例驱逐的“脏数据”,防止访问无效内存。
如果移除此锁,unordered_set 在并发读写时会因 rehash 导致数据竞争崩溃,且竞态条件下可能返回已删除记录,破坏系统正确性。

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • StorageManager应该已经对里面的map上了一个锁了?

  • 对于避免读到已经被驱逐的脏数据,现在join operator里面应该是实现了增大缓冲区大小的,缓冲区足够大就不会读到脏数据了,这样用空间换锁的时间比较好。

  • 然后controller是比较底层获取索引的接口,你这样在这里上锁以后,如果有一些不需要共享的方法也要用索引,也会用到这个controller的接口,这样就会凭空多出一个锁的开销了,有一个思路是新建一类controller专门用来处理共享环境下的索引,比如shared_controller,然后blank_controller保留无锁的样子?但是这个改动也比较大,可以不在这个pr里面解决

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我让copilot写了一个方案:
将 ConcurrencyController 体系拆分为两类实现:BlankController(无锁,用于独立索引场景)和 SharedController(有锁,用于共享索引场景)。BlankController 恢复为原始的轻量实现,不包含 local_uids_ 和锁,查询时直接返回 Index 结果;SharedController 继承共享隔离逻辑,内部维护 local_uids_ 集合和读写锁,在查询时过滤出本 Controller 插入的数据。使用方根据场景选择合适的 Controller 类型:S3J 分区索引、ClusteredJoin 等每个 subtask 拥有独立索引的场景使用 BlankController;多流共享 StorageManager、HNSW/IVF 全局索引等场景使用 SharedController。

后续我先挂一个issue?

mutable std::shared_mutex local_uids_mutex_;
};
} // namespace sageFlow
82 changes: 82 additions & 0 deletions include/coordination/workset_directory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#pragma once

#include <cstdint>
#include <optional>
#include <unordered_map>
#include <shared_mutex>
#include <mutex>
#include <vector>
#include <atomic>

namespace sageFlow {

struct WorksetProfile {
uint64_t id;
int owner;
double load;
};

/**
* @brief Interface for managing Workset ownership and Load info.
*/
class WorksetDirectory {
public:
virtual ~WorksetDirectory() = default;

virtual std::optional<int> getOwner(uint64_t workset_id) const = 0;
virtual void setOwner(uint64_t workset_id, int worker_id) = 0;

// Report load for a specific workset (accumulative or absolute? let's say absolute rate)
virtual void reportWorksetLoad(uint64_t workset_id, double load) = 0;

// Get global view for rebalancing
virtual std::vector<WorksetProfile> getAllWorksetProfiles() const = 0;
};
Comment thread
ZeroJustMe marked this conversation as resolved.

class LocalWorksetDirectory : public WorksetDirectory {
public:
std::optional<int> getOwner(uint64_t workset_id) const override {
std::shared_lock<std::shared_mutex> lock(mutex_);
auto it = owners_.find(workset_id);
if (it != owners_.end()) {
return it->second;
}
return std::nullopt;
}

void setOwner(uint64_t workset_id, int worker_id) override {
std::unique_lock<std::shared_mutex> lock(mutex_);
owners_[workset_id] = worker_id;
}

void reportWorksetLoad(uint64_t workset_id, double load) override {
std::lock_guard<std::mutex> lock(load_mutex_);
loads_[workset_id] = load;
}

std::vector<WorksetProfile> getAllWorksetProfiles() const override {
std::shared_lock<std::shared_mutex> owner_lock(mutex_);
std::lock_guard<std::mutex> load_lock(load_mutex_);

std::vector<WorksetProfile> profiles;
profiles.reserve(owners_.size());

for (const auto& kv : owners_) {
double load = 0.0;
if (loads_.count(kv.first)) {
load = loads_.at(kv.first);
}
profiles.push_back({kv.first, kv.second, load});
}
return profiles;
}

private:
mutable std::shared_mutex mutex_;
std::unordered_map<uint64_t, int> owners_;

mutable std::mutex load_mutex_;
std::unordered_map<uint64_t, double> loads_;
};

} // namespace sageFlow
4 changes: 4 additions & 0 deletions include/execution/blocking_queue.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class BlockingQueue final : public IQueue {
* 并使后续的 push 调用立即返回,pop 调用在队列为空后返回 std::nullopt。
*/
void stop() override;

bool isStopped() const override {
return stopped_.load(std::memory_order_acquire);
}

private:
std::queue<TaggedResponse> queue_;
Expand Down
Loading