Mix.install([
{:ex_data_sketch, "~> 0.10"}
])Introduction
LLM token streams pose unique analytics challenges: high cardinality (millions of tokens), throughput demands (100K+ tokens/sec per model), and the need for real-time monitoring across multiple dimensions. Probabilistic sketches provide O(1)-per-update data structures that answer these questions in bounded memory.
This Livebook demonstrates using ExDataSketch for AI/ML workloads: tracking unique users, token distributions, frequent prompt patterns, and membership filtering.
Section 1: Unique User and Session Tracking
Track distinct users and sessions across a token stream using HLL:
# Simulate a stream of 100K inference requests from 5000 unique users
requests = for i <- 1..100_000 do
user_id = "user_#{rem(i, 5000)}"
session_id = "session_#{rem(i, 20_000)}"
tokens = 50 + :rand.uniform(450)
%{user_id: user_id, session_id: session_id, tokens: tokens}
end
# Count unique users and sessions
unique_users = ExDataSketch.HLL.from_enumerable(
Stream.map(requests, & &1.user_id),
p: 14
)
unique_sessions = ExDataSketch.ULL.from_enumerable(
Stream.map(requests, & &1.session_id),
p: 14
)
IO.puts("Unique users estimate: #{Float.round(ExDataSketch.HLL.estimate(unique_users), 0)} (true: 5000)")
IO.puts("Unique sessions estimate: #{Float.round(ExDataSketch.ULL.estimate(unique_sessions), 0)} (true: 20000)")
IO.puts("User HLL memory: #{ExDataSketch.HLL.size_bytes(unique_users)} bytes")
IO.puts("Session ULL memory: #{ExDataSketch.ULL.size_bytes(unique_sessions)} bytes")Section 2: Token Distribution Analysis
Track the distribution of output token counts using DDSketch and KLL:
# Extract token counts from requests
token_counts = Enum.map(requests, & &1.tokens)
# DDSketch: quantile-centric distribution sketch
ddsketch = ExDataSketch.DDSketch.from_enumerable(token_counts, alpha: 0.01)
IO.puts("Token distribution (DDSketch):")
IO.puts(" p50: #{Float.round(ExDataSketch.DDSketch.quantile(ddsketch, 0.50), 1)}")
IO.puts(" p90: #{Float.round(ExDataSketch.DDSketch.quantile(ddsketch, 0.90), 1)}")
IO.puts(" p99: #{Float.round(ExDataSketch.DDSketch.quantile(ddsketch, 0.99), 1)}")
IO.puts(" Memory: #{ExDataSketch.DDSketch.size_bytes(ddsketch)} bytes")
# KLL: more compact quantile sketch
kll = ExDataSketch.KLL.from_enumerable(token_counts, k: 200)
IO.puts("\nToken distribution (KLL):")
IO.puts(" p50: #{Float.round(ExDataSketch.KLL.quantile(kll, 0.50), 1)}")
IO.puts(" p90: #{Float.round(ExDataSketch.KLL.quantile(kll, 0.90), 1)}")
IO.puts(" p99: #{Float.round(ExDataSketch.KLL.quantile(kll, 0.99), 1)}")
IO.puts(" Memory: #{ExDataSketch.KLL.size_bytes(kll)} bytes")Section 3: Frequent Prompt Patterns
Identify the most common prompt prefixes or model calls using MisraGries and FrequentItems:
# Simulate prompt patterns: some models are much more popular
prompt_patterns = for i <- 1..100_000 do
cond do
rem(i, 3) == 0 -> "gpt-4o/chat"
rem(i, 5) == 0 -> "gpt-4o/completion"
rem(i, 7) == 0 -> "claude-3.5/chat"
true -> "gpt-4o-mini/chat"
end
end
# MisraGries: memory-bounded heavy hitter detection
mg = ExDataSketch.MisraGries.from_enumerable(prompt_patterns, k: 10)
IO.puts("MisraGries top patterns:")
for {item, count} <- ExDataSketch.MisraGries.top_k(mg, 5) do
IO.puts(" #{item}: ~#{count} (estimated)")
end
# FrequentItems: similar but with different guarantees.
# Note: `top_k/2` takes a keyword list (limit: N); returned maps expose
# :item, :estimate, :error, :lower, :upper (no :count field).
fi = ExDataSketch.FrequentItems.from_enumerable(prompt_patterns, k: 10)
IO.puts("\nFrequentItems top patterns:")
for item <- ExDataSketch.FrequentItems.top_k(fi, limit: 5) do
IO.puts(" #{item.item}: ~#{item.estimate}")
endSection 4: CMS for Rate Tracking
Count-Min Sketch tracks per-key frequency in bounded space, ideal for tracking token usage per user or per model:
# Track per-model token usage with heavy-hitter distribution
# CMS works best when a few keys dominate the stream
# Simulate: gpt-4o-mini is 50% of traffic, claude-3.5 is 33%, gpt-4o is 17%
models = Stream.map(1..100_000, fn i ->
cond do
rem(i, 2) == 0 -> "gpt-4o-mini"
rem(i, 3) == 0 -> "gpt-4o"
true -> "claude-3.5"
end
end)
cms = ExDataSketch.CMS.from_enumerable(models, width: 1024, depth: 5)
IO.puts("CMS frequency estimates (heavy-hitter detection):")
IO.puts(" gpt-4o-mini: ~#{ExDataSketch.CMS.estimate(cms, "gpt-4o-mini")} (true: 50000)")
IO.puts(" gpt-4o: ~#{ExDataSketch.CMS.estimate(cms, "gpt-4o")} (true: ~16667)")
IO.puts(" claude-3.5: ~#{ExDataSketch.CMS.estimate(cms, "claude-3.5")} (true: ~33333)")
IO.puts("CMS memory: #{ExDataSketch.CMS.size_bytes(cms)} bytes")Section 5: Bloom Filters for Deduplication
Bloom filters allow fast approximate deduplication of prompt hashes:
# Check if a prompt has been seen before (approximate)
bloom = ExDataSketch.Bloom.from_enumerable(
Stream.map(1..50_000, fn i -> "prompt_hash_#{i}" end),
capacity: 100_000
)
IO.puts("Bloom filter membership:")
IO.puts(" Seen prompt 42: #{ExDataSketch.Bloom.member?(bloom, "prompt_hash_42")}")
IO.puts(" Seen prompt 99999: #{ExDataSketch.Bloom.member?(bloom, "prompt_hash_99999")}")
IO.puts(" Unseen prompt: #{ExDataSketch.Bloom.member?(bloom, "prompt_hash_unseen")}")
IO.puts(" Memory: #{ExDataSketch.Bloom.size_bytes(bloom)} bytes")Section 6: Multi-Dimensional Dashboard
Combine multiple sketches for a comprehensive real-time view:
# Build a composite dashboard of sketches
dashboard = %{
unique_users: ExDataSketch.HLL.from_enumerable(Stream.map(requests, & &1.user_id), p: 12),
unique_sessions: ExDataSketch.ULL.from_enumerable(Stream.map(requests, & &1.session_id), p: 12),
token_dist: ExDataSketch.DDSketch.from_enumerable(Stream.map(requests, & &1.tokens), alpha: 0.01),
request_freq: ExDataSketch.CMS.new(width: 256, depth: 5)
|> then(fn cms ->
Enum.reduce(requests, cms, fn req, acc -> ExDataSketch.CMS.update(acc, req.user_id) end)
end)
}
total_memory = ExDataSketch.HLL.size_bytes(dashboard.unique_users) +
ExDataSketch.ULL.size_bytes(dashboard.unique_sessions) +
ExDataSketch.DDSketch.size_bytes(dashboard.token_dist) +
ExDataSketch.CMS.size_bytes(dashboard.request_freq)
IO.puts("=== AI Token Analytics Dashboard ===")
IO.puts("Unique users: #{Float.round(ExDataSketch.HLL.estimate(dashboard.unique_users), 0)} (true: 5000)")
IO.puts("Unique sessions: #{Float.round(ExDataSketch.ULL.estimate(dashboard.unique_sessions), 0)} (true: 20000)")
IO.puts("Token p50: #{Float.round(ExDataSketch.DDSketch.quantile(dashboard.token_dist, 0.50), 1)}")
IO.puts("Token p99: #{Float.round(ExDataSketch.DDSketch.quantile(dashboard.token_dist, 0.99), 1)}")
IO.puts("Total dashboard memory: #{total_memory} bytes (~#{Float.round(total_memory / 1024, 1)} KB)")
IO.puts("(vs ~#{Float.round(100_000 * 50 / 1024, 0)} KB for raw request storage)")Section 7: Operational Guidance
Sketch selection for AI workloads:
| Question | Sketch | Memory | Error |
|---|---|---|---|
| How many unique users? | HLL p=14 | 16KB | ~0.8% |
| How many unique sessions? | ULL p=14 | 16KB | ~0.6% |
| Token count distribution? | DDSketch alpha=0.01 | ~5KB | 1% quantile error |
| Most frequent models? | MisraGries k=10 | ~1KB | top-k heavy hitters |
| Per-user request count? | CMS w=1024,d=5 | ~5KB | ~1% overestimate for heavy hitters |
| Prompt deduplication? | Bloom cap=100K | ~120KB | ~1% false positive |
Update throughput: All sketches support >1M updates/sec per core in NIF mode. For multi-billion token streams, use partition-local aggregation then merge.