import gc
import statistics
import time
from dataclasses import dataclass
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
@dataclass
class BenchmarkResult:
size_bytes: int
sequential_ms: float
rayon_ms: float
sequential_throughput: float rayon_throughput: float speedup: float
def create_test_text(target_size: int) -> str:
base = "The quick brown fox jumps over the lazy dog. "
repeat = max(1, target_size // len(base))
text = base * repeat
return text[:target_size] if len(text) > target_size else text
def benchmark_size(size: int, warmup: int = 20, iterations: int = 50) -> BenchmarkResult:
import splintr
text = create_test_text(size)
actual_size = len(text.encode("utf-8"))
enc = splintr.Tokenizer.from_pretrained("cl100k_base")
for _ in range(warmup):
enc.encode(text)
enc.encode_rayon(text)
gc.collect()
seq_times = []
for _ in range(iterations):
start = time.perf_counter_ns()
enc.encode(text)
end = time.perf_counter_ns()
seq_times.append((end - start) / 1e6)
rayon_times = []
for _ in range(iterations):
start = time.perf_counter_ns()
enc.encode_rayon(text)
end = time.perf_counter_ns()
rayon_times.append((end - start) / 1e6)
seq_avg = statistics.mean(seq_times)
rayon_avg = statistics.mean(rayon_times)
seq_throughput = actual_size / seq_avg / 1000 rayon_throughput = actual_size / rayon_avg / 1000
speedup = seq_avg / rayon_avg
return BenchmarkResult(
size_bytes=actual_size,
sequential_ms=seq_avg,
rayon_ms=rayon_avg,
sequential_throughput=seq_throughput,
rayon_throughput=rayon_throughput,
speedup=speedup,
)
def check_rayon_available():
try:
import splintr
enc = splintr.Tokenizer.from_pretrained("cl100k_base")
if not hasattr(enc, 'encode_rayon'):
print("ERROR: encode_rayon method not found!")
print("You need to add encode_rayon to the Tokenizer class.")
print("\nAdd this to src/core/tokenizer.rs:")
print("""
/// Encode text using Rayon parallel processing.
/// Use this for very large texts (>100KB) where parallelization helps.
pub fn encode_rayon(&self, text: &str) -> Vec<u32> {
// ... parallel implementation
}
""")
return False
return True
except Exception as e:
print(f"Error: {e}")
return False
def run_benchmarks() -> list[BenchmarkResult]:
results = []
sizes = [
100, 200, 500,
1000, 2000, 5000,
10000, 20000, 50000,
100000, 200000, 500000,
1000000,
]
print("\n" + "=" * 80)
print("SEQUENTIAL vs RAYON BENCHMARK")
print("=" * 80)
print(f"\n{'Size':>12} {'Sequential':>12} {'Rayon':>12} {'Seq MB/s':>10} {'Rayon MB/s':>10} {'Speedup':>10}")
print("-" * 80)
for size in sizes:
result = benchmark_size(size)
results.append(result)
speedup_str = f"{result.speedup:.2f}x"
if result.speedup > 1.1:
speedup_str += " (rayon wins)"
elif result.speedup < 0.9:
speedup_str += " (seq wins)"
print(
f"{result.size_bytes:>10} B "
f"{result.sequential_ms:>10.3f} ms "
f"{result.rayon_ms:>10.3f} ms "
f"{result.sequential_throughput:>10.1f} "
f"{result.rayon_throughput:>10.1f} "
f"{speedup_str:>14}"
)
return results
def find_crossover(results: list[BenchmarkResult]) -> int | None:
for i, r in enumerate(results):
if r.speedup > 1.0:
remaining = results[i:]
if all(rr.speedup >= 0.95 for rr in remaining): return r.size_bytes
return None
def generate_chart(results: list[BenchmarkResult], output_path: str):
sizes = [r.size_bytes for r in results]
seq_throughput = [r.sequential_throughput for r in results]
rayon_throughput = [r.rayon_throughput for r in results]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
ax1.plot(sizes, seq_throughput, 'o-', label='Sequential', color='#2ecc71', linewidth=2, markersize=6)
ax1.plot(sizes, rayon_throughput, 's-', label='Rayon (parallel)', color='#e74c3c', linewidth=2, markersize=6)
ax1.set_xscale('log')
ax1.set_xlabel('Text Size (bytes)', fontsize=12)
ax1.set_ylabel('Throughput (MB/s)', fontsize=12)
ax1.set_title('Sequential vs Rayon Throughput', fontsize=14, fontweight='bold')
ax1.legend()
ax1.grid(True, alpha=0.3)
crossover = find_crossover(results)
if crossover:
ax1.axvline(x=crossover, color='gray', linestyle='--', alpha=0.7)
ax1.annotate(
f'Crossover\n~{crossover/1000:.0f}KB',
xy=(crossover, max(seq_throughput) * 0.8),
fontsize=10,
ha='center',
)
speedups = [r.speedup for r in results]
colors = ['#2ecc71' if s < 1 else '#e74c3c' for s in speedups]
ax2.bar(range(len(sizes)), speedups, color=colors)
ax2.axhline(y=1.0, color='black', linestyle='-', linewidth=1)
ax2.set_xticks(range(len(sizes)))
ax2.set_xticklabels([f'{s/1000:.0f}K' if s >= 1000 else str(s) for s in sizes], rotation=45, ha='right')
ax2.set_xlabel('Text Size', fontsize=12)
ax2.set_ylabel('Speedup (Rayon / Sequential)', fontsize=12)
ax2.set_title('Rayon Speedup Ratio (>1 = Rayon faster)', fontsize=14, fontweight='bold')
ax2.grid(axis='y', alpha=0.3)
from matplotlib.patches import Patch
legend_elements = [
Patch(facecolor='#2ecc71', label='Sequential wins'),
Patch(facecolor='#e74c3c', label='Rayon wins'),
]
ax2.legend(handles=legend_elements, loc='upper left')
plt.tight_layout()
plt.savefig(output_path, dpi=150, bbox_inches='tight')
print(f"\nChart saved to: {output_path}")
plt.close()
def main():
print("=" * 80)
print("SPLINTR: SEQUENTIAL vs RAYON SINGLE TEXT ENCODING")
print("=" * 80)
if not check_rayon_available():
return
output_dir = Path(__file__).parent / "results"
output_dir.mkdir(exist_ok=True)
print("\nWarming up...")
import splintr
enc = splintr.Tokenizer.from_pretrained("cl100k_base")
warmup_text = "warmup " * 1000
for _ in range(50):
enc.encode(warmup_text)
enc.encode_rayon(warmup_text)
print("Warmup complete.")
results = run_benchmarks()
crossover = find_crossover(results)
print("\n" + "=" * 80)
if crossover:
print(f"CROSSOVER POINT: ~{crossover:,} bytes ({crossover/1024:.1f} KB)")
print(f"Recommendation: Use Rayon for texts > {crossover:,} bytes")
else:
print("No clear crossover found - Sequential is generally faster")
print("=" * 80)
generate_chart(results, str(output_dir / "benchmark_splintr.png"))
print("\nDone!")
if __name__ == "__main__":
main()