Skip to content

Commit 275e89a

Browse files
committed
feat: Owner-Computes dedup via routing_mask bitmask
Eliminate cross-subtask and IQ bidirectional duplicate matches inside JoinOperator, reducing Sink dedup pressure to zero. - Add routing_mask_ (uint64_t bitmask) to VectorRecord - Set routing_mask in ResultPartition::emit() for broadcast/multicast/unicast - Owner-Computes rule in JoinOperator emit: only lowest-indexed subtask in intersection(query_mask, cand_mask) emits a match - Propagate candidate routing_mask through JoinFunction result - Add combinedMatchId() for fallback dedup path - Add owner_dedup_count metric Results (ClusteredJoin p=8, k=8): Before: total_emits=3,091,648 sink_dedup=2,705,192 After: total_emits=386,456 sink_dedup=0 Recall=1.0000 Precision=1.0000
1 parent 613ad56 commit 275e89a

5 files changed

Lines changed: 87 additions & 2 deletions

File tree

‎include/common/data_types.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ struct VectorRecord {
4949
const int64_t timestamp_; // Timestamp for the record
5050
VectorData data_; // Shared pointer to the vector data
5151

52+
/// Bitmask of subtask indices this record was routed to (multicast routing).
53+
/// Bit i set ⇒ record lives in subtask i's WindowState.
54+
/// Used by Owner-Computes dedup: only the lowest-indexed subtask in the
55+
/// intersection of query_mask & candidate_mask emits a match.
56+
/// Supports up to 64 subtasks (uint64_t). 0 = unset / legacy path.
57+
uint64_t routing_mask_ = 0;
58+
5259
// Constructor with move semantics for efficiency
5360
VectorRecord(const uint64_t &uid, const int64_t &timestamp, VectorData &&data);
5461

‎include/operator/join_operator.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ namespace sageFlow {
105105
private:
106106
enum class InternalIndexKind { NONE, IVF, BRUTEFORCE, VAMANA, HDR_TREE }; // 可扩展
107107

108+
/// Deterministic hash combining for match pair (left_uid, right_uid).
109+
/// Used in fallback dedup path when routing_mask is unavailable.
110+
static uint64_t combinedMatchId(uint64_t left_uid, uint64_t right_uid) {
111+
uint64_t x = left_uid ^ (right_uid + 0x9e3779b97f4a7c15ULL + (left_uid << 6) + (left_uid >> 2));
112+
return x;
113+
}
114+
108115
bool createIndexPair(IndexType type, const std::string& prefix);
109116
bool createIndexPair(IndexType type, const std::string& prefix, const IndexParameters& params);
110117

‎include/utils/metrics/join_metrics.h‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ struct JoinMetrics {
4848
std::atomic<uint64_t> total_records_left{0}; ///< Total records processed on left side
4949
std::atomic<uint64_t> total_records_right{0}; ///< Total records processed on right side
5050
std::atomic<uint64_t> total_emits{0}; ///< Total results emitted
51+
std::atomic<uint64_t> owner_dedup_count{0}; ///< Matches skipped by owner-computes rule
5152
std::atomic<uint64_t> window_records_left_completed{0}; ///< Records expired from left window
5253
std::atomic<uint64_t> window_records_right_completed{0}; ///< Records expired from right window
5354

@@ -89,7 +90,7 @@ struct JoinMetrics {
8990
join_function_ns = emit_ns = lock_wait_ns = 0;
9091
window_insert_count = index_op_count = expire_count = candidate_fetch_count = similarity_count =
9192
join_function_count = emit_count = lock_wait_count = 0;
92-
total_records_left = total_records_right = total_emits = 0;
93+
total_records_left = total_records_right = total_emits = owner_dedup_count = 0;
9394
window_records_left_completed = window_records_right_completed = 0;
9495
apply_processing_ns = apply_processing_count = e2e_latency_ns = e2e_latency_count = 0;
9596
qiq_q1_ns = qiq_q1_count = qiq_insert_ns = qiq_insert_count = qiq_q2_ns = qiq_q2_count = 0;
@@ -115,7 +116,7 @@ struct JoinMetrics {
115116
EMIT(similarity_ns) EMIT(join_function_ns) EMIT(emit_ns) EMIT(lock_wait_ns)
116117
EMIT(window_insert_count) EMIT(index_op_count) EMIT(expire_count) EMIT(candidate_fetch_count)
117118
EMIT(similarity_count) EMIT(join_function_count) EMIT(emit_count) EMIT(lock_wait_count)
118-
EMIT(total_records_left) EMIT(total_records_right) EMIT(total_emits)
119+
EMIT(total_records_left) EMIT(total_records_right) EMIT(total_emits) EMIT(owner_dedup_count)
119120
EMIT(window_records_left_completed) EMIT(window_records_right_completed)
120121
EMIT(apply_processing_ns) EMIT(apply_processing_count)
121122
EMIT(e2e_latency_ns) EMIT(e2e_latency_count)

‎src/execution/result_partition.cpp‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,21 @@ void ResultPartition::emit(Response&& data, int slot) const {
4747
// 注意:isBroadcast() 可能在 partition() 调用后改变状态(训练完成后变为 false)
4848
if (partitioner_->isBroadcast()) {
4949
// 广播模式:将数据发送到所有通道
50+
// Build routing_mask: all channels are targets
51+
uint64_t route_mask = 0;
52+
for (size_t i = 0; i < output_channels_.size() && i < 64; ++i) {
53+
route_mask |= (uint64_t{1} << i);
54+
}
55+
5056
for (size_t i = 0; i < output_channels_.size(); ++i) {
5157
if (i == output_channels_.size() - 1) {
5258
// 最后一个通道,移动数据
59+
if (data.record_) data.record_->routing_mask_ = route_mask;
5360
pushWithRetry(output_channels_[i], {std::move(data), slot});
5461
} else {
5562
// 其他通道,复制数据
5663
Response data_copy{data.type_, data.record_ ? std::make_unique<VectorRecord>(*data.record_) : nullptr};
64+
if (data_copy.record_) data_copy.record_->routing_mask_ = route_mask;
5765
pushWithRetry(output_channels_[i], {std::move(data_copy), slot});
5866
}
5967
}
@@ -62,23 +70,36 @@ void ResultPartition::emit(Response&& data, int slot) const {
6270
// 这用于边界向量复制(如 ClusteredJoin 的 multicast_k > 1)
6371
auto target_channels = partitioner_->partitionMulti(data, output_channels_.size());
6472

73+
// Build routing_mask from multicast targets
74+
uint64_t route_mask = 0;
75+
for (size_t ch : target_channels) {
76+
if (ch < 64) route_mask |= (uint64_t{1} << ch);
77+
}
78+
6579
if (target_channels.size() == 1) {
6680
// 只有一个目标,直接移动
81+
if (data.record_) data.record_->routing_mask_ = route_mask;
6782
pushWithRetry(output_channels_[target_channels[0]], {std::move(data), slot});
6883
} else {
6984
// 多个目标,复制数据到前 n-1 个,移动到最后一个
7085
for (size_t i = 0; i < target_channels.size(); ++i) {
7186
if (i == target_channels.size() - 1) {
87+
if (data.record_) data.record_->routing_mask_ = route_mask;
7288
pushWithRetry(output_channels_[target_channels[i]], {std::move(data), slot});
7389
} else {
7490
Response data_copy{data.type_, data.record_ ? std::make_unique<VectorRecord>(*data.record_) : nullptr};
91+
if (data_copy.record_) data_copy.record_->routing_mask_ = route_mask;
7592
pushWithRetry(output_channels_[target_channels[i]], {std::move(data_copy), slot});
7693
}
7794
}
7895
}
7996
} else {
8097
// 单播模式:发送到单个目标分区
8198
size_t channel_index = partitioner_->partition(data, output_channels_.size());
99+
// Single target: set routing_mask to just that channel
100+
if (data.record_ && channel_index < 64) {
101+
data.record_->routing_mask_ = (uint64_t{1} << channel_index);
102+
}
82103
pushWithRetry(output_channels_[channel_index], {std::move(data), slot});
83104
}
84105
}

‎src/operator/join_operator.cpp‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,9 @@ void JoinOperator::executeJoinWithState(
975975
t_joinF.stop();
976976
t_similarity.resume();
977977
if (res.record_) {
978+
// Propagate candidate's routing_mask into the join result so that
979+
// the Owner-Computes dedup in the emit stage can use it.
980+
res.record_->routing_mask_ = cand->routing_mask_;
978981
local_return_pool.emplace_back(left_slot_id_, std::move(res.record_));
979982
}
980983
} catch (const std::exception& e) {
@@ -1243,6 +1246,15 @@ auto JoinOperator::apply(Response&& record, int slot, Collector& collector,
12431246
}
12441247
}
12451248

1249+
// Build routing bitmask for Owner-Computes dedup.
1250+
// Bit i set ⇒ record lives in subtask i's WindowState.
1251+
uint64_t route_mask = 0;
1252+
for (size_t t : target_subtasks) {
1253+
if (t < 64) route_mask |= (uint64_t{1} << t);
1254+
}
1255+
data_ptr->routing_mask_ = route_mask;
1256+
data_for_join->routing_mask_ = route_mask;
1257+
12461258
for (size_t target_subtask : target_subtasks) {
12471259
auto data_for_insert = std::make_unique<VectorRecord>(*data_ptr);
12481260
updateSideWithState(current_state, index_id, std::move(data_for_insert), now_time_stamp, slot, target_subtask);
@@ -1349,7 +1361,44 @@ auto JoinOperator::apply(Response&& record, int slot, Collector& collector,
13491361
// 发送 Join 结果
13501362
{
13511363
MetricsTimer t_emit(JoinMetrics::instance().emit_ns);
1364+
// Owner-Computes dedup: for each match (query, candidate), only the
1365+
// lowest-indexed subtask that holds BOTH records emits the result.
1366+
// routing_mask_ is a bitmask of subtasks each record was routed to.
1367+
// intersection = query_mask & cand_mask ⇒ subtasks holding both.
1368+
// owner = __builtin_ctzll(intersection) ⇒ lowest such subtask.
1369+
// If current_subtask == owner → emit. Otherwise → skip.
1370+
//
1371+
// Fallback: if either routing_mask_ is 0 (unset / legacy path),
1372+
// fall back to intra-apply hash-set dedup (apply_seen) as before.
1373+
std::unordered_set<uint64_t> apply_seen; // only used in fallback path
13521374
for (auto& p : local_return_pool) {
1375+
if (p.second && parallelism_ > 1) {
1376+
const uint64_t left_uid = (p.first == left_slot_id_) ? p.second->uid_ : data_for_join->uid_;
1377+
const uint64_t right_uid = (p.first == left_slot_id_) ? data_for_join->uid_ : p.second->uid_;
1378+
1379+
const uint64_t query_mask = data_for_join->routing_mask_;
1380+
const uint64_t cand_mask = p.second->routing_mask_;
1381+
1382+
if (query_mask != 0 && cand_mask != 0) {
1383+
// Owner-Computes path (preferred)
1384+
const uint64_t intersection = query_mask & cand_mask;
1385+
if (intersection == 0) {
1386+
// Should not happen: defensive emit anyway.
1387+
} else {
1388+
const size_t owner = static_cast<size_t>(__builtin_ctzll(intersection));
1389+
if (owner != subtask_index) {
1390+
metrics_increment(JoinMetrics::instance().owner_dedup_count);
1391+
continue; // another subtask is the owner
1392+
}
1393+
}
1394+
} else {
1395+
// Fallback: routing_mask not set
1396+
const uint64_t cid = combinedMatchId(left_uid, right_uid);
1397+
if (!apply_seen.insert(cid).second) {
1398+
continue;
1399+
}
1400+
}
1401+
}
13531402
Response out{ResponseType::Record, std::move(p.second)};
13541403
collector.collect(std::make_unique<Response>(std::move(out)), p.first);
13551404
metrics_increment(JoinMetrics::instance().total_emits);

0 commit comments

Comments
 (0)