Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ flowchart TD
data_storage --> common["common"]

manager --> event["event"]
event --> protocol["protocol(proto/grpc)"]
manager --> metrics["metrics"]
service --> metrics
service --> config
service --> data_storage
manager --> protocol["protocol(proto/grpc)"]
manager --> protocol

%% 有意的反向边(近似环,改动需谨慎)
common -. request_context .-> metrics
Expand Down
7 changes: 5 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,11 @@ kvcm.metrics.enable_prometheus=true
# Prometheus metrics名称前缀,默认kvcm
kvcm.metrics.prometheus_prefix=kvcm

# log event publisher的初始化配置值,暂未启用
kvcm.event.event_publishers_configs
# Event publisher 配置。log 默认开启;optimizer.enable=true 时,会在现有
# kvcm.service.rpc_port 上注册 OptimizerEventStreamService,不新增监听端口。
# 每个 publisher 的 queue_size 是它自己的发布队列上限;max_subscribers 是并发订阅数上限,
# subscriber_queue_size 是每个订阅者的独立缓冲上限;字段省略时使用下列默认值。
kvcm.event.event_publishers_configs={"log":{"enable":true,"queue_size":10000},"optimizer":{"enable":true,"queue_size":100000,"max_subscribers":4,"subscriber_queue_size":10000}}
```

### SchedulePlanExecutor 线程与迁移预算
Expand Down
7 changes: 4 additions & 3 deletions docs/design/module_architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ KVCache Manager 采用中心化部署,负责 KVCache 的全局元数据管理
|---|---|---|
| **common** | `common/` | 基础设施层:日志、JSON、错误码、Redis 客户端、`RequestContext`(逐请求追踪上下文)、`concurrent_hash_map`、`lru_cache`、`loop_thread`、服务发现、崩溃处理等。几乎所有 C++ 模块都依赖它。 |
| **metrics** | `metrics/` | 可观测性。`MetricsRegistry`/`MetricsCollector` 收集指标,多种 reporter(kmonitor/local/logging/dummy)上报,`PrometheusExporter` 通过 HTTP 暴露。 |
| **event** | `event/` | 轻量事件总线。`EventManager` 将领域事件(如 cache 回收事件)分发给注册的 `EventPublisher`(默认 `LogEventPublisher`)。 |
| **protocol** | `protocol/protobuf/` | gRPC/proto 契约。定义 meta/admin/debug/kv_meta 服务,生成 C++ 与 Python 桩。 |
| **event** | `event/` | 轻量事件总线。`EventManager` 将领域事件分发给注册的 `EventPublisher`;除默认日志发布器外,`OptimizerEventPublisher` 会将缓存读取事件转换为 protocol 中的 `TraceQueryRequest`,再经 `SubscriptionEventSink` 交给 service 层的 gRPC 流。 |
| **protocol** | `protocol/protobuf/` | gRPC/proto 契约。定义 meta/admin/debug/kv_meta 以及 optimizer 事件流服务,生成 C++ 与 Python 桩。 |

### 客户端与连接器

Expand Down Expand Up @@ -83,7 +83,7 @@ client 通过 `InitParams.role_type` 区分角色:**SCHEDULER**(调度节点
service → manager → meta → config → data_storage → common
```

- `common`、`protocol` 是最底层的通用模块,被各层广泛依赖。
- `common`、`protocol` 是最底层的通用模块,被各层广泛依赖;`event` 的 optimizer 发布链路直接使用 `protocol` 定义的 `TraceQueryRequest`
- `metrics`、`event` 是通用支撑模块,被 `manager` 与 `service` 复用。
- `service` 在启动时实例化 `CacheManager`(注入 `MetricsRegistry` + `RegistryManager`),并通过 `config` 的 `LeaderElector` 门控 recover/cleanup。

Expand Down Expand Up @@ -138,6 +138,7 @@ flowchart TD

%% 通用支撑依赖
manager --> event
event --> protocol
manager --> metrics
service --> metrics
service --> event
Expand Down
33 changes: 33 additions & 0 deletions kv_cache_manager/event/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ cc_library(
"//kv_cache_manager/common:logger",
"//kv_cache_manager/common:string_util",
":base_event",
":event_publishers_config",
"//kv_cache_manager/event/spec_events",
],
)
Expand All @@ -29,3 +30,35 @@ cc_library(
"//kv_cache_manager/common:jsonizable",
],
)

cc_library(
name = "event_publishers_config",
srcs = [
"event_publishers_config.cc",
],
hdrs = [
"event_publishers_config.h",
],
deps = [
"//kv_cache_manager/common:jsonizable",
],
)

cc_library(
name = "optimizer_event_publisher",
srcs = [
"optimizer_event_publisher.cc",
],
hdrs = [
"optimizer_event_publisher.h",
],
deps = [
":base_event",
":event_publisher",
":event_publishers_config",
"//kv_cache_manager/common:logger",
"//kv_cache_manager/event/optimizer_stream:event_sink",
"//kv_cache_manager/event/spec_events",
"//kv_cache_manager/protocol/protobuf:service_cc_proto",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Document the new event-to-protocol dependency

This target introduces a direct event → protocol module dependency, but neither the root dependency diagram nor docs/design/module_architecture.md was updated; the latter still depicts event as depending only on common. Update both architecture diagrams and the event module description so future dependency checks account for this new edge.

AGENTS.md reference: AGENTS.md:L38-L38

Useful? React with 👍 / 👎.

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.

已修复。根目录 AGENTS.md 与 docs/design/module_architecture.md 的依赖图均补充了 event → protocol 边,并更新 event/protocol 模块说明,记录 OptimizerEventPublisher、TraceQueryRequest、SubscriptionEventSink 与 gRPC 事件流的关系。

],
)
44 changes: 44 additions & 0 deletions kv_cache_manager/event/event_publishers_config.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#include "kv_cache_manager/event/event_publishers_config.h"

namespace kv_cache_manager {

bool LogEventPublisherConfig::FromRapidValue(const rapidjson::Value &rapid_value) {
KVCM_JSON_GET_DEFAULT_MACRO(rapid_value, "enable", enable_, true);
KVCM_JSON_GET_DEFAULT_MACRO(rapid_value, "queue_size", queue_size_, std::size_t(10000));
return queue_size_ > 0;
}

void LogEventPublisherConfig::ToRapidWriter(rapidjson::Writer<rapidjson::StringBuffer> &writer) const noexcept {
Put(writer, "enable", enable_);
Put(writer, "queue_size", queue_size_);
}

bool OptimizerEventPublisherConfig::FromRapidValue(const rapidjson::Value &rapid_value) {
KVCM_JSON_GET_DEFAULT_MACRO(rapid_value, "enable", enable_, false);
KVCM_JSON_GET_DEFAULT_MACRO(rapid_value, "queue_size", queue_size_, std::size_t(100000));
KVCM_JSON_GET_DEFAULT_MACRO(rapid_value, "max_subscribers", max_subscribers_, std::size_t(4));
KVCM_JSON_GET_DEFAULT_MACRO(rapid_value, "subscriber_queue_size", subscriber_queue_size_, std::size_t(10000));
return queue_size_ > 0 && max_subscribers_ > 0 && subscriber_queue_size_ > 0;
}

void OptimizerEventPublisherConfig::ToRapidWriter(rapidjson::Writer<rapidjson::StringBuffer> &writer) const noexcept {
Put(writer, "enable", enable_);
Put(writer, "queue_size", queue_size_);
Put(writer, "max_subscribers", max_subscribers_);
Put(writer, "subscriber_queue_size", subscriber_queue_size_);
}

bool EventPublishersConfig::FromRapidValue(const rapidjson::Value &rapid_value) {
log_ = LogEventPublisherConfig{};
optimizer_ = OptimizerEventPublisherConfig{};
KVCM_JSON_GET_MACRO(rapid_value, "log", log_);
KVCM_JSON_GET_MACRO(rapid_value, "optimizer", optimizer_);
return true;
}

void EventPublishersConfig::ToRapidWriter(rapidjson::Writer<rapidjson::StringBuffer> &writer) const noexcept {
Put(writer, "log", log_);
Put(writer, "optimizer", optimizer_);
}

} // namespace kv_cache_manager
54 changes: 54 additions & 0 deletions kv_cache_manager/event/event_publishers_config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#pragma once

#include <cstddef>

#include "kv_cache_manager/common/jsonizable.h"

namespace kv_cache_manager {

class LogEventPublisherConfig : public Jsonizable {
public:
bool FromRapidValue(const rapidjson::Value &rapid_value) override;
void ToRapidWriter(rapidjson::Writer<rapidjson::StringBuffer> &writer) const noexcept override;

bool enable() const { return enable_; }
std::size_t queue_size() const { return queue_size_; }

private:
bool enable_ = true;
std::size_t queue_size_ = 10000;
};

class OptimizerEventPublisherConfig : public Jsonizable {
public:
bool FromRapidValue(const rapidjson::Value &rapid_value) override;
void ToRapidWriter(rapidjson::Writer<rapidjson::StringBuffer> &writer) const noexcept override;

bool enable() const { return enable_; }
std::size_t queue_size() const { return queue_size_; }
std::size_t max_subscribers() const { return max_subscribers_; }
std::size_t subscriber_queue_size() const { return subscriber_queue_size_; }

private:
bool enable_ = false;
std::size_t queue_size_ = 100000;
std::size_t max_subscribers_ = 4;
std::size_t subscriber_queue_size_ = 10000;
};

class EventPublishersConfig : public Jsonizable {
public:
bool FromRapidValue(const rapidjson::Value &rapid_value) override;
void ToRapidWriter(rapidjson::Writer<rapidjson::StringBuffer> &writer) const noexcept override;

bool enable_log_event_publisher() const { return log_.enable(); }
const LogEventPublisherConfig &log_event_publisher_config() const { return log_; }
bool enable_optimizer_event_publisher() const { return optimizer_.enable(); }
const OptimizerEventPublisherConfig &optimizer_event_publisher_config() const { return optimizer_; }

private:
LogEventPublisherConfig log_;
OptimizerEventPublisherConfig optimizer_;
};

} // namespace kv_cache_manager
12 changes: 6 additions & 6 deletions kv_cache_manager/event/log_event_publisher.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@
#include "kv_cache_manager/common/logger.h"
namespace kv_cache_manager {

LogEventPublisher::LogEventPublisher() = default;
LogEventPublisher::LogEventPublisher() : LogEventPublisher(LogEventPublisherConfig{}) {}

LogEventPublisher::LogEventPublisher(const LogEventPublisherConfig &config) : config_(config) {}

LogEventPublisher::~LogEventPublisher() {
if (running_) {
Stop();
}
}
// 先假设这里传的就是log文件路径
bool LogEventPublisher::Init(const std::string &config) {
// 初始化基础队列,这里的队列长度可以通过配置传入
InitBasicQueue();
bool LogEventPublisher::Init(const std::string & /*config*/) {
InitBasicQueue(config_.queue_size());

running_ = true;

Expand Down Expand Up @@ -88,4 +88,4 @@ std::string LogEventPublisher::FormatEvent(const std::shared_ptr<BaseEvent> &eve
return event->ToJsonString();
}

} // namespace kv_cache_manager
} // namespace kv_cache_manager
6 changes: 5 additions & 1 deletion kv_cache_manager/event/log_event_publisher.h
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#pragma once

#include <cstddef>
#include <fstream>
#include <memory>
#include <string>
#include <thread>
#include <vector>

#include "kv_cache_manager/event/event_publisher.h"
#include "kv_cache_manager/event/event_publishers_config.h"

namespace kv_cache_manager {
class BaseEvent;
Expand All @@ -20,6 +22,7 @@ namespace kv_cache_manager {
class LogEventPublisher : public EventPublisher {
public:
LogEventPublisher();
explicit LogEventPublisher(const LogEventPublisherConfig &config);
~LogEventPublisher() override;

bool Init(const std::string &config) override;
Expand All @@ -32,7 +35,8 @@ class LogEventPublisher : public EventPublisher {
std::string FormatEvent(const std::shared_ptr<BaseEvent> &event) const;

private:
LogEventPublisherConfig config_;
std::thread worker_;
};

} // namespace kv_cache_manager
} // namespace kv_cache_manager
123 changes: 123 additions & 0 deletions kv_cache_manager/event/optimizer_event_publisher.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#include "kv_cache_manager/event/optimizer_event_publisher.h"

#include <utility>

#include "kv_cache_manager/common/logger.h"
#include "kv_cache_manager/event/base_event.h"
#include "kv_cache_manager/event/spec_events/optimizer_event.h"
#include "kv_cache_manager/protocol/protobuf/optimizer_service.pb.h"

namespace kv_cache_manager {

OptimizerEventPublisher::OptimizerEventPublisher(std::shared_ptr<EventSink> sink,
const OptimizerEventPublisherConfig &config)
: sink_(std::move(sink)), config_(config) {}

OptimizerEventPublisher::~OptimizerEventPublisher() {
if (running_) {
Stop();
}
}

bool OptimizerEventPublisher::Init(const std::string & /*config*/) {
if (!sink_) {
KVCM_LOG_ERROR("OptimizerEventPublisher: no sink provided");
return false;
}
InitBasicQueue(config_.queue_size());
running_ = true;
worker_ = std::thread(&OptimizerEventPublisher::WorkerThread, this);
KVCM_LOG_INFO("OptimizerEventPublisher: initialized, queue_size=%zu", config_.queue_size());
return true;
}

bool OptimizerEventPublisher::Publish(const std::shared_ptr<BaseEvent> &event) {
if (!event || !running_) {
return false;
}
// Runs on a serving thread: enqueue and return, nothing else. A full queue
// drops the event (counted by the base class) without surfacing an expected
// best-effort drop as a publish failure and triggering a warning per request.
BasicEnqueue(event);
return true;
}

bool OptimizerEventPublisher::Stop() {
if (!running_) {
return true;
}
running_ = false;
ClearBasicQueue();
// Required, not belt-and-braces: BasicWait() blocks in
// condition_variable::wait(), whose predicate is only re-evaluated when it
// is notified. Clearing running_ alone leaves the worker asleep and join()
// below would never return.
if (basic_queue_) {
basic_queue_->queue_cv.notify_all();
}
if (worker_.joinable()) {
worker_.join();
}
if (sink_) {
sink_->Stop();
}
KVCM_LOG_INFO("OptimizerEventPublisher: stopped, forwarded=%zu skipped=%zu queue_dropped=%zu",
forwarded_.load(),
skipped_.load(),
BasicDroppedCount());
return true;
}

void OptimizerEventPublisher::WorkerThread() {
// A single worker keeps conversion and delivery off serving threads
// without adding synchronization between multiple consumers of the queue.
while (running_) {
BasicWait();

std::shared_ptr<BaseEvent> event;
while (BasicDequeue(event)) {
proto::optimizer::TraceQueryRequest request;
if (!Convert(event, &request)) {
skipped_.fetch_add(1);
continue;
}
if (sink_->Send(request)) {
forwarded_.fetch_add(1);
}
}
}
}

bool OptimizerEventPublisher::Convert(const std::shared_ptr<BaseEvent> &event,
proto::optimizer::TraceQueryRequest *out) {
// Every publisher registered with EventManager sees every event, so write
// and reclaim events arrive here too. Only cache reads can be replayed.
const auto *get_event = dynamic_cast<const CacheGetEvent *>(event.get());
if (get_event == nullptr) {
return false;
}

out->set_trace_id(get_event->trace_id());
// CacheGetEvent's source is the instance the request was served for.
out->set_instance_id(get_event->event_source());
for (const auto key : get_event->get_keys()) {
out->add_block_keys(key);
}
Comment on lines +103 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not replay batch lookups as prefix queries

When KVCM serves QT_BATCH_GET, this conversion forwards the arbitrary batch key list as a TraceQueryRequest even though that protocol defines block_keys as an ordered prefix and the online optimizer computes continuous prefix hits from it. Because the conversion also drops CacheGetEvent::query_type(), a batch such as hit/miss/hit is indistinguishable from a prompt prefix and is accounted with prefix-hit semantics, corrupting aggregate hit rates and capacity curves. Filter out non-prefix-compatible query types or carry their query semantics through to a matching replay implementation.

Useful? React with 👍 / 👎.

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.

按 KVCM 职责边界不在这里过滤。KVCM 只透明采集和转发已发生的 Query,不理解或重解释 QT_BATCH_GET 等具体查询语义;如不同查询类型需要不同回放模型,应由协议语义和 Optimizer 消费实现统一处理。

// The event carries microseconds; the replay works in nanoseconds.
out->set_timestamp_ns(get_event->event_trigger_time_us() * 1000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Capture access timestamps before executing the lookup

For a slow metadata or backend lookup, this forwards a completion-time timestamp rather than the time the cache access occurred: each producing CacheManager method constructs the CacheGetEvent and calls SetEventTriggerTime() only after its lookup has returned. Fresh evidence beyond the previous timestamp discussions is this producer-side capture point, which shifts every delayed access forward by its request latency and therefore changes TTL expiration and refresh decisions even if the optimizer correctly consumes timestamp_ns. Capture the request or lookup start time and carry that value instead.

Useful? React with 👍 / 👎.


// Only the token count is needed, never the ids. An empty tokens vector
// means the caller passed pre-computed keys instead, and then the exact
// input length is simply not known here: 0 says "unknown" and the
// consumer infers it from block count times block size. That inference
// loses the trailing partial block, which biases the hit rate upwards -
// the opposite direction from dropped events, so the two do not cancel.
out->set_input_token_len(static_cast<std::int64_t>(get_event->get_tokens().size()));

// An empty block_keys list is legitimate, not junk: a prompt shorter than
// one block has no complete block. Its input_token_len still belongs in
// the hit-rate denominator, so the event is forwarded as-is.
return true;
}

} // namespace kv_cache_manager
Loading
Loading