File size: 6,379 Bytes
44810a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
"""
Benchmark: Complexity and Scaling Analysis (Paper Experiment 2 & 3)
Evaluates:
1. Offline BFS Build Time (ms) vs Number of States |S|
2. Memory Footprint (MB) vs Number of States |S|
3. Online Token Masking Latency (us) vs Number of States |S| (Empirical O(1) Verification)

Generates publication-quality figure: 'paper_figure_scaling.png'
"""

import os
import time
import torch
import matplotlib.pyplot as plt
from tabulate import tabulate
from typing import List, Dict

from core.fsm_builder import ReachabilityFSM
from core.logit_processor import GoalReachabilityLogitsProcessor


def run_scaling_benchmark(output_plot_path: str = "paper_figure_scaling.png"):
    print("\n" + "=" * 80)
    print(" [EXPERIMENT 2 & 3] FSM Complexity & O(1) Latency Scaling Benchmark")
    print("=" * 80)

    device = "cpu"  # Run on CPU for strict baseline consistency
    vocab_sizes = [32000, 151643]  # Standard 32k vs Qwen 151k
    state_counts = [10, 50, 200, 1000, 5000, 10000]
    max_budget = 50
    num_latency_trials = 500

    results: Dict[int, Dict[str, List]] = {
        v: {"states": [], "build_time_ms": [], "memory_mb": [], "online_latency_us": []}
        for v in vocab_sizes
    }

    table_rows = []

    for vocab_size in vocab_sizes:
        print(f"\n--- Testing Vocab Size: {vocab_size:,} ---")
        for num_states in state_counts:
            # 1. Build FSM structure
            fsm = ReachabilityFSM(num_states=num_states, vocab_size=vocab_size, device=device)
            # Add chain transitions + some branches
            for s in range(num_states - 1):
                fsm.add_transition(s, token_id=s % vocab_size, to_state=s + 1)
                # Add branching transition
                if s + 2 < num_states:
                    fsm.add_transition(s, token_id=(s * 7 + 13) % vocab_size, to_state=s + 2)

            fsm.set_goal_states([num_states - 1])

            # 2. Measure Offline BFS Build Time
            start_b = time.perf_counter()
            fsm.build_reachability(max_steps=max_budget)
            end_b = time.perf_counter()
            build_time_ms = (end_b - start_b) * 1000

            # 3. Measure Memory Footprint
            mem_bytes = fsm.memory_footprint_bytes()
            mem_mb = mem_bytes / (1024 * 1024)

            # 4. Measure Online 1-Token Masking Latency
            processor = GoalReachabilityLogitsProcessor(fsm=fsm, max_budget=max_budget)
            dummy_input = torch.tensor([[0]], dtype=torch.long, device=device)
            dummy_scores = torch.randn((1, vocab_size), device=device)

            # Warmup
            for _ in range(50):
                _ = processor(dummy_input, dummy_scores.clone())

            start_l = time.perf_counter()
            for _ in range(num_latency_trials):
                _ = processor(dummy_input, dummy_scores)
            end_l = time.perf_counter()

            online_latency_us = ((end_l - start_l) / num_latency_trials) * 1e6

            results[vocab_size]["states"].append(num_states)
            results[vocab_size]["build_time_ms"].append(build_time_ms)
            results[vocab_size]["memory_mb"].append(mem_mb)
            results[vocab_size]["online_latency_us"].append(online_latency_us)

            table_rows.append([
                f"{vocab_size:,}",
                f"{num_states:,}",
                f"{build_time_ms:.2f} ms",
                f"{mem_mb:.2f} MB",
                f"{online_latency_us:.2f} us ({online_latency_us/1000:.4f} ms)",
            ])

    headers = [
        "Vocab Size",
        "States |S|",
        "Offline BFS Time",
        "Memory Footprint",
        "Online 1-Token Latency",
    ]
    print("\n" + tabulate(table_rows, headers=headers, tablefmt="grid"))

    # -------------------------------------------------------------
    # Generate Publication-Quality Figures (Matplotlib)
    # -------------------------------------------------------------
    print(f"\n[PLOT] Generating publication-quality figures -> {output_plot_path}...")
    plt.style.use("seaborn-v0_8-whitegrid" if "seaborn-v0_8-whitegrid" in plt.style.available else "default")
    fig, axes = plt.subplots(1, 3, figsize=(18, 5), dpi=300)

    colors = {32000: "#1f77b4", 151643: "#ff7f0e"}
    labels = {32000: "Vocab: 32,000 (LLaMA/Mistral)", 151643: "Vocab: 151,643 (Qwen2.5)"}

    # Plot 1: Offline BFS Build Time
    for v in vocab_sizes:
        axes[0].plot(
            results[v]["states"],
            results[v]["build_time_ms"],
            marker="o",
            linewidth=2,
            color=colors[v],
            label=labels[v],
        )
    axes[0].set_title("(a) Offline BFS Precomputation Time", fontsize=12, fontweight="bold")
    axes[0].set_xlabel("FSM State Count |S|", fontsize=11)
    axes[0].set_ylabel("Build Time (ms)", fontsize=11)
    axes[0].set_xscale("log")
    axes[0].legend(fontsize=9)
    axes[0].grid(True, linestyle="--", alpha=0.6)

    # Plot 2: Memory Footprint
    for v in vocab_sizes:
        axes[1].plot(
            results[v]["states"],
            results[v]["memory_mb"],
            marker="s",
            linewidth=2,
            color=colors[v],
            label=labels[v],
        )
    axes[1].set_title("(b) Memory Footprint (VRAM / RAM)", fontsize=12, fontweight="bold")
    axes[1].set_xlabel("FSM State Count |S|", fontsize=11)
    axes[1].set_ylabel("Memory (MB)", fontsize=11)
    axes[1].set_xscale("log")
    axes[1].legend(fontsize=9)
    axes[1].grid(True, linestyle="--", alpha=0.6)

    # Plot 3: Online 1-Token Latency (O(1) Verification)
    for v in vocab_sizes:
        axes[2].plot(
            results[v]["states"],
            results[v]["online_latency_us"],
            marker="^",
            linewidth=2,
            color=colors[v],
            label=labels[v],
        )
    axes[2].set_title("(c) Online Runtime Latency: Strict O(1)", fontsize=12, fontweight="bold")
    axes[2].set_xlabel("FSM State Count |S|", fontsize=11)
    axes[2].set_ylabel("Latency per Token (us)", fontsize=11)
    axes[2].set_xscale("log")
    axes[2].legend(fontsize=9)
    axes[2].grid(True, linestyle="--", alpha=0.6)

    plt.tight_layout()
    plt.savefig(output_plot_path, bbox_inches="tight")
    plt.close()
    print(f"[SUCCESS] Figure successfully saved to: {os.path.abspath(output_plot_path)}\n")


if __name__ == "__main__":
    run_scaling_benchmark()