Skip to content

从 Sink 收集详细指标 #26

Description

@moonandlife

https://github.com/intellistream/SAGE/blob/5170cd3d50d42eca98d202d5f532e43322a92094/benchmark/experiments/distributed_workloads/workload4/pipeline.py#L650

"""
Workload 4 Pipeline Factory
============================

整合所有算子,构建完整的 Workload 4 分布式数据流。

Pipeline 结构:
1. 双流源(Query + Document)
2. Embedding 预计算
3. Semantic Join (60s 大窗口)
4. 双路 VDB 检索(4-stage each)
5. 图遍历内存检索
6. 结果汇聚
7. DBSCAN 聚类去重
8. 5维评分重排序
9. MMR 多样性过滤
10. 双层 Batch 聚合
11. 批量 LLM 生成
12. Metrics Sink
"""

from __future__ import annotations

import time
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from sage.kernel.api.local_environment import LocalEnvironment
    from sage.kernel.api.remote_environment import RemoteEnvironment

try:
    # 流汇聚和分流工具
    from .aggregation import MergeAllResultsJoin, MergeVDBResultsJoin
    from .batching import CategoryBatchAggregator, GlobalBatchAggregator
    from .clustering import DBSCANClusteringOperator
    from .config import Workload4Config
    from .generation import BatchLLMGenerator, Workload4MetricsSink
    from .graph_memory import GraphMemoryRetriever

    # 🔧 临时添加:单源测试用的转换器
    from .mappers import QueryToJoinedMapper
    from .models import (
        BatchContext,
        ClusteringResult,
        DocumentEvent,
        GraphMemoryResult,
        JoinedEvent,
        QueryEvent,
        RerankingResult,
        VDBRetrievalResult,
        Workload4Metrics,
    )
    from .reranking import MMRDiversityFilter, MultiDimensionalReranker
    from .semantic_join import SemanticJoinOperator
    from .sources import (
        EmbeddingPrecompute,
        Workload4DocumentSource,
        Workload4QuerySource,
    )
    from .tag_utils import TagFilter, TagMapper
    from .vdb_retrieval import (
        LocalReranker,
        StageAggregator,
        VDBResultFilter,
        VDBRetriever,
    )
except ImportError:
    # 流汇聚和分流工具
    from aggregation import MergeVDBResultsJoin
    from batching import CategoryBatchAggregator, GlobalBatchAggregator
    from clustering import DBSCANClusteringOperator
    from generation import BatchLLMGenerator, Workload4MetricsSink
    from graph_memory import GraphMemoryRetriever

    # 🔧 临时添加:单源测试用的转换器
    from models import (
        Workload4Metrics,
    )
    from reranking import MMRDiversityFilter, MultiDimensionalReranker
    from semantic_join import SemanticJoinOperator
    from sources import (
        EmbeddingPrecompute,
        Workload4DocumentSource,
        Workload4QuerySource,
    )
    from tag_utils import TagFilter, TagMapper
    from vdb_retrieval import (
        VDBRetriever,
    )

    from config import Workload4Config


# =============================================================================
# Service Registration
# =============================================================================


def register_embedding_service(
    env: LocalEnvironment | RemoteEnvironment,
    config: Workload4Config,
) -> bool:
    """
    注册 Embedding 服务。

    使用远端 Embedding API(OpenAI 兼容)。
    """
    try:
        from .services import EmbeddingService

        env.register_service(
            "embedding",
            EmbeddingService,
            base_url=config.embedding_base_url,
            model=config.embedding_model,
        )

        print(f"✓ Registered embedding_service: {config.embedding_base_url}")
        return True

    except Exception as e:
        print(f"✗ Failed to register embedding_service: {e}")
        return False


def register_vdb_services(
    env: LocalEnvironment | RemoteEnvironment,
    config: Workload4Config,
) -> dict[str, bool]:
    """
    注册双路 VDB 服务(vdb1 和 vdb2)。

    使用真实的 FiQA 数据集(57,638 文档,1024 维)。
    vdb1 和 vdb2 共享相同的 FAISS 索引(fiqa_faiss.index)。
    索引和文档存储在 config.vdb_index_dir。

    **数据源**:/home/sage/data/fiqa_faiss.index + fiqa_documents.jsonl
    """
    results = {}

    # Import Service class from module
    from .services import FAISSVDBService

    for vdb_name in ["vdb1", "vdb2"]:
        try:
            env.register_service(
                vdb_name,
                FAISSVDBService,
                vdb_name=vdb_name,
                dimension=config.embedding_dimension,
                index_dir=config.vdb_index_dir,
                dataset_name=config.vdb_dataset_name,
            )

            results[vdb_name] = True
            print(f"✓ Registered {vdb_name} (FiQA dataset, shared index)")

        except Exception as e:
            results[vdb_name] = False
            print(f"✗ Failed to register {vdb_name}: {e}")
            import traceback

            traceback.print_exc()

    return results


def register_graph_memory_service(
    env: LocalEnvironment | RemoteEnvironment,
    config: Workload4Config,
) -> bool:
    """
    注册图内存服务。

    使用 Mock 图结构或 NeuroMem Graph backend。
    """
    # Import Service class from module
    from .services import GraphMemoryService

    try:
        env.register_service(
            "graph_memory",
            GraphMemoryService,
            max_depth=config.graph_max_depth,
            max_nodes=config.graph_max_nodes,
        )

        print("✓ Registered graph_memory_service")
        return True

    except Exception as e:
        print(f"✗ Failed to register graph_memory_service: {e}")
        return False


def register_llm_service(
    env: LocalEnvironment | RemoteEnvironment,
    config: Workload4Config,
) -> bool:
    """
    注册 LLM 服务。

    使用远端 LLM API(OpenAI 兼容)。
    """
    # Import Service class from module
    from .services import LLMService

    try:
        env.register_service(
            "llm",
            LLMService,
            base_url=config.llm_base_url,
            model=config.llm_model,
            max_tokens=config.llm_max_tokens,
        )

        print(f"✓ Registered llm_service: {config.llm_base_url}")
        return True

    except Exception as e:
        print(f"✗ Failed to register llm_service: {e}")
        return False


def register_all_services(
    env: LocalEnvironment | RemoteEnvironment,
    config: Workload4Config,
) -> dict[str, bool]:
    """
    注册所有必要的 services。

    Returns:
        服务注册结果字典
    """
    results = {}

    print("\n" + "=" * 80)
    print("Registering Workload 4 Services")
    print("=" * 80)

    # 1. Embedding Service
    results["embedding"] = register_embedding_service(env, config)

    # 2. VDB Services (vdb1, vdb2)
    vdb_results = register_vdb_services(env, config)
    results.update(vdb_results)

    # 3. Graph Memory Service
    results["graph_memory"] = register_graph_memory_service(env, config)

    # 4. LLM Service
    results["llm"] = register_llm_service(env, config)

    print("=" * 80)
    print(f"Service Registration Summary: {sum(results.values())}/{len(results)} successful")
    print("=" * 80)

    return results


# =============================================================================
# Pipeline Factory
# =============================================================================


class Workload4Pipeline:
    """
    Workload 4 Pipeline 工厂。

    整合所有算子,构建完整的分布式数据流。
    """

    def __init__(self, config: Workload4Config):
        """
        初始化 Pipeline。

        Args:
            config: Workload 4 配置
        """
        self.config = config
        self.env = None
        self.metrics = None

    def _create_environment(self, name: str):
        """创建执行环境(本地或远程)"""
        if self.config.use_remote:
            from pathlib import Path

            from sage.kernel.api.remote_environment import RemoteEnvironment

            # workload4 所在目录(当前文件的父目录的父目录)
            workload_dir = str(Path(__file__).parent.parent)

            # RemoteEnvironment 参数:name, config, host, port, scheduler, extra_python_paths
            env = RemoteEnvironment(
                name=name,
                scheduler=self.config.scheduler_type,  # "fifo" 或 "load_aware"
                extra_python_paths=[workload_dir],  # 让远程节点能找到 workload4 模块
            )
            return env
        else:
            from sage.kernel.api.local_environment import LocalEnvironment

            return LocalEnvironment(name=name)

    def build(self, name: str = "workload4_benchmark") -> Workload4Pipeline:
        """
        构建完整 pipeline。

        Pipeline 结构:
        1. 双流源(Query + Document)
        2. Embedding 预计算
        3. Semantic Join (60s 大窗口, parallelism=16)
        4. 图遍历内存检索
        5. 双路 VDB 检索(4-stage each)
        6. 汇聚所有检索结果
        7. DBSCAN 聚类去重
        8. 5维评分重排序
        9. MMR 多样性过滤
        10. 双层 Batch 聚合
        11. 批量 LLM 生成
        12. Metrics Sink

        Returns:
            self (支持链式调用)
        """
        print("\n" + "=" * 80)
        print("Building Workload 4 Pipeline")
        print("=" * 80)
        print(f"Pipeline Name: {name}")
        print(f"Use Remote: {self.config.use_remote}")
        print(f"Num Nodes: {self.config.num_nodes}")
        print(f"Num Tasks: {self.config.num_tasks}")
        print(f"Duration: {self.config.duration}s")
        print("=" * 80)

        # === 1. 创建环境 ===
        # CRITICAL: Create environment in a local scope and build pipeline immediately
        # This avoids storing RemoteEnvironment in self, which causes serialization issues
        env = self._create_environment(name)
        print(f"✓ Created {'Remote' if self.config.use_remote else 'Local'}Environment")
        if self.config.use_remote:
            print(f"  Scheduler: {self.config.scheduler_type}")

        # === 2. 注册所有 services ===
        service_results = register_all_services(env, self.config)

        if not all(service_results.values()):
            failed = [k for k, v in service_results.items() if not v]
            print(f"⚠️  Some services failed to register: {failed}")
            print("   Pipeline may not work correctly.")

        # === 3. 构建双流源 ===
        print("\n" + "=" * 80)
        print("Building Data Streams")
        print("=" * 80)

        # Query 流(传入类而非实例)
        query_stream = env.from_source(
            Workload4QuerySource,
            num_tasks=self.config.num_tasks,
            qps=self.config.query_qps,
            query_types=list(self.config.query_type_distribution.keys()),
            categories=list(self.config.category_distribution.keys()),
            use_fiqa=False,  # 可配置
        )
        print(f"✓ Created Query Stream (QPS={self.config.query_qps})")

        # Document 流(传入类而非实例)
        doc_stream = env.from_source(
            Workload4DocumentSource,
            num_docs=self.config.num_tasks * 20,  # 每个query对应20个doc
            qps=self.config.doc_qps,
            categories=list(self.config.category_distribution.keys()),
        )
        print(f"✓ Created Document Stream (QPS={self.config.doc_qps})")

        # === 4. Embedding 预计算 ===
        query_stream = query_stream.map(
            EmbeddingPrecompute,
            embedding_base_url=self.config.embedding_base_url,
            embedding_model=self.config.embedding_model,
            batch_size=32,
            field_name="query_text",
        )
        print("✓ Added EmbeddingPrecompute for Query Stream")

        doc_stream = doc_stream.map(
            EmbeddingPrecompute,
            embedding_base_url=self.config.embedding_base_url,
            embedding_model=self.config.embedding_model,
            batch_size=32,
            field_name="doc_text",
        )
        print("✓ Added EmbeddingPrecompute for Document Stream")

        # === 5. Semantic Join ===
        print("\n" + "=" * 80)
        print("Building Semantic Join")
        print("=" * 80)

        # SemanticJoinOperator 是 BaseCoMapFunction,使用 comap 而不是 join
        # 定义兼容 StopSignal 的 key selector
        def joined_key_selector(x):
            """从 JoinedEvent 提取 joined_id,兼容 StopSignal"""
            from sage.kernel.runtime.communication.packet import StopSignal

            if isinstance(x, StopSignal):
                return x  # StopSignal 直接返回自身,让它继续传递
            return hash(x.joined_id) % self.config.join_parallelism

        joined_stream = query_stream.connect(doc_stream).comap(
            SemanticJoinOperator,
            window_seconds=self.config.join_window_seconds,
            threshold=self.config.join_threshold,
            max_matches=self.config.join_max_matches,
            batch_compute=True,
        )
        # .keyby(joined_key_selector)
        print(
            f"✓ Added Semantic Join (window={self.config.join_window_seconds}s, "
            f"threshold={self.config.join_threshold}, "
            f"parallelism={self.config.join_parallelism})"
        )

        # === 6. 图遍历内存检索(串行第一步)===
        print("\n" + "=" * 80)
        print("Building Graph Memory Retrieval (串行第一步)")
        print("=" * 80)

        graph_stream = joined_stream.map(
            GraphMemoryRetriever,
            max_depth=self.config.graph_max_depth,
            max_nodes=self.config.graph_max_nodes,
            beam_width=self.config.graph_bfs_beam_width,
        )
        print(
            f"✓ Added Graph Memory Retrieval (max_depth={self.config.graph_max_depth}, "
            f"max_nodes={self.config.graph_max_nodes})"
        )

        # === 7. 双路 VDB 检索(使用 Tag+Filter 模式)===
        # Tag+Filter 模式:每条数据都复制到两个分支
        # graph_stream → tag("vdb1"|"vdb2") → filter(vdb1) → VDBRetriever(vdb1)
        #                                    → filter(vdb2) → VDBRetriever(vdb2)
        print("\n" + "=" * 80)
        print("Building VDB Retrieval Branches (Tag+Filter 模式)")
        print("=" * 80)

        # 给每条数据打上 vdb1 和 vdb2 标签(flatmap 会复制两份)
        tagged_stream = graph_stream.flatmap(
            TagMapper,
            tags=["vdb1", "vdb2"],
        )
        print("✓ Added TagMapper (tags=['vdb1', 'vdb2'])")

        # VDB1 分支:过滤出 tag=vdb1 的数据
        vdb1_filtered = tagged_stream.filter(
            TagFilter,
            target_tag="vdb1",
        ).sink(
            Workload4MetricsSink,
            metrics_output_dir=self.config.metrics_output_dir,
            verbose=True,
        )
        self.env = env
        return self
        vdb1_stream = vdb1_filtered.map(
            VDBRetriever,
            vdb_name="vdb1",
            top_k=self.config.vdb1_top_k,
            stage=1,
        )
        print(f"✓ Added VDB1 Branch (tag=vdb1, top_k={self.config.vdb1_top_k})")

        # VDB2 分支:过滤出 tag=vdb2 的数据
        vdb2_filtered = tagged_stream.filter(
            TagFilter,
            target_tag="vdb2",
        )
        vdb2_stream = vdb2_filtered.map(
            VDBRetriever,
            vdb_name="vdb2",
            top_k=self.config.vdb2_top_k,
            stage=1,
        )
        print(f"✓ Added VDB2 Branch (tag=vdb2, top_k={self.config.vdb2_top_k})")

        # === 8. 汇聚所有检索结果(使用 keyby + join)===
        # 现在只需要合并 VDB1 + VDB2(graph 已经在上游)
        # 流程:VDB1 + VDB2 → Join → 合并结果
        print("\n" + "=" * 80)
        print("Building Result Aggregation (keyby + join, VDB1 + VDB2)")
        print("=" * 80)

        # 定义兼容 StopSignal 的 key selector
        def vdb_key_selector(x):
            """从 VDBResultsWrapper 提取 query_id,兼容 StopSignal"""
            from sage.kernel.runtime.communication.packet import StopSignal

            if isinstance(x, StopSignal):
                return x  # StopSignal 直接返回自身,让它继续传递
            return x.query_id  # VDBResultsWrapper 有 query_id 字段

        # VDB1 和 VDB2 结果合并(按 query_id)
        # 注意:因为 graph 在上游串行,VDB 结果已经包含 graph 信息
        vdb1_keyed = vdb1_stream.keyby(vdb_key_selector)
        vdb2_keyed = vdb2_stream.keyby(vdb_key_selector)

        all_results = vdb1_keyed.connect(vdb2_keyed).join(
            MergeVDBResultsJoin,
            parallelism=self.config.join_parallelism,
        )
        print("✓ Added VDB1+VDB2 Join (graph 已在上游串行执行)")

        # === 9. DBSCAN 聚类去重 ===
        print("\n" + "=" * 80)
        print("Building Clustering & Deduplication")
        print("=" * 80)

        deduplicated_stream = all_results.map(
            DBSCANClusteringOperator,
            eps=self.config.dbscan_eps,
            min_samples=self.config.dbscan_min_samples,
            metric="cosine",
        )
        print(
            f"✓ Added DBSCAN Clustering (eps={self.config.dbscan_eps}, "
            f"min_samples={self.config.dbscan_min_samples})"
        )

        # === 10. 5维评分重排序 ===
        print("\n" + "=" * 80)
        print("Building Reranking")
        print("=" * 80)

        reranked_stream = deduplicated_stream.map(
            MultiDimensionalReranker,
            score_weights=self.config.rerank_score_weights,
            top_k=self.config.rerank_top_k,
        )
        print(
            f"✓ Added MultiDimensional Reranking (5 dimensions, top_k={self.config.rerank_top_k})"
        )

        # === 11. MMR 多样性过滤 ===
        reranked_stream = reranked_stream.map(
            MMRDiversityFilter,
            lambda_param=self.config.mmr_lambda,
            top_k=self.config.rerank_top_k,
        )
        print(f"✓ Added MMR Diversity Filter (lambda={self.config.mmr_lambda})")

        # === 12. 双层 Batch 聚合 ===
        print("\n" + "=" * 80)
        print("Building Batch Aggregation")
        print("=" * 80)

        # 第一层: Category Batch
        category_batched = reranked_stream.keyby(
            lambda x: x.query.category  # 按 category 分组
        ).map(
            CategoryBatchAggregator,
            batch_size=self.config.category_batch_size,
            timeout_ms=self.config.category_batch_timeout_ms,
        )
        print(
            f"✓ Added Category Batch (size={self.config.category_batch_size}, "
            f"timeout={self.config.category_batch_timeout_ms}ms)"
        )

        # 第二层: Global Batch
        global_batched = category_batched.map(
            GlobalBatchAggregator,
            batch_size=self.config.global_batch_size,
            timeout_ms=self.config.global_batch_timeout_ms,
        )
        print(
            f"✓ Added Global Batch (size={self.config.global_batch_size}, "
            f"timeout={self.config.global_batch_timeout_ms}ms)"
        )

        # === 13. 批量 LLM 生成 ===
        print("\n" + "=" * 80)
        print("Building LLM Generation")
        print("=" * 80)

        generated_stream = global_batched.map(
            BatchLLMGenerator,
            llm_base_url=self.config.llm_base_url,
            llm_model=self.config.llm_model,
            max_tokens=self.config.llm_max_tokens,
        )
        print(f"✓ Added Batch LLM Generator (model={self.config.llm_model})")

        # === 14. Metrics Sink ===
        print("\n" + "=" * 80)
        print("Building Metrics Sink")
        print("=" * 80)

        generated_stream.sink(
            Workload4MetricsSink,
            metrics_output_dir=self.config.metrics_output_dir,
            verbose=True,
        )
        print(f"✓ Added Metrics Sink (output_dir={self.config.metrics_output_dir})")

        print("\n" + "=" * 80)
        print("Pipeline Build Complete")
        print("=" * 80)

        # Store environment for run()
        self.env = env

        return self

    def run(self) -> Workload4Metrics:
        """
        执行 pipeline。

        Returns:
            Workload4Metrics: 汇总指标
        """
        if self.env is None:
            raise RuntimeError("Pipeline not built. Call build() first.")

        print("\n" + "=" * 80)
        print("Starting Workload 4 Execution")
        print("=" * 80)
        print(f"Duration: {self.config.duration}s")
        print(f"Expected Tasks: {self.config.num_tasks}")
        print("=" * 80)

        start_time = time.time()

        try:
            # 执行 pipeline
            self.env.submit(autostop=True)

            end_time = time.time()
            elapsed = end_time - start_time

            print("\n" + "=" * 80)
            print("Workload 4 Execution Complete")
            print("=" * 80)
            print(f"Elapsed Time: {elapsed:.2f}s")
            print("=" * 80)

            # 收集汇总指标
            # TODO: 从 Sink 收集详细指标
            self.metrics = Workload4Metrics(
                task_id="summary",
                query_id="summary",
                query_arrival_time=start_time,
                doc_arrival_time=start_time,
                join_time=0.0,
                vdb1_start_time=0.0,
                vdb1_end_time=0.0,
                vdb2_start_time=0.0,
                vdb2_end_time=0.0,
                graph_start_time=0.0,
                graph_end_time=0.0,
                clustering_time=0.0,
                reranking_time=0.0,
                batch_time=0.0,
                generation_time=0.0,
                end_to_end_time=elapsed,
                join_matched_docs=0,
                vdb1_results=0,
                vdb2_results=0,
                graph_nodes_visited=0,
                clusters_found=0,
                duplicates_removed=0,
                final_top_k=0,
                cpu_time=0.0,
                memory_peak_mb=0.0,
            )

            return self.metrics

        except Exception as e:
            print(f"\n✗ Pipeline execution failed: {e}")
            import traceback

            traceback.print_exc()
            raise


# =============================================================================
# Convenience Functions
# =============================================================================


def create_workload4_pipeline(
    config: Workload4Config | None = None, **config_overrides
) -> Workload4Pipeline:
    """
    创建 Workload 4 Pipeline(便捷函数)。

    Args:
        config: Workload4Config 实例(可选)
        **config_overrides: 覆盖配置项

    Returns:
        Workload4Pipeline 实例

    Example:
        >>> pipeline = create_workload4_pipeline(num_tasks=50, duration=600)
        >>> pipeline.build().run()
    """
    if config is None:
        config = Workload4Config()

    # 应用覆盖
    for key, value in config_overrides.items():
        if hasattr(config, key):
            setattr(config, key, value)
        else:
            print(f"⚠️  Unknown config key: {key}")

    return Workload4Pipeline(config)


def run_workload4(config: Workload4Config | None = None, **config_overrides) -> Workload4Metrics:
    """
    一键运行 Workload 4(便捷函数)。

    Args:
        config: Workload4Config 实例(可选)
        **config_overrides: 覆盖配置项

    Returns:
        Workload4Metrics: 汇总指标

    Example:
        >>> metrics = run_workload4(num_tasks=100, use_remote=True)
    """
    pipeline = create_workload4_pipeline(config, **config_overrides)
    pipeline.build()
    return pipeline.run()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions