A sequence-of-actions transformer that detects inauthentic (bot / spam / coordinated) accounts from their behavioral event streams, together with the task-head training stack and a reference streaming scoring pipeline.
bdsm/
├── runtime/ Backbone, task heads, scoring pipeline, results sink
├── training/ Task-head training on cached backbone activations
├── proto/ Protobuf definitions for scoring events
└── rust/ Rust components (Kafka accumulator, PyO3 feature extractor)
runtime/model.py — a bidirectional transformer encoder over a user's recent
action sequence, with:
- Time-aware RoPE: rotary position embeddings driven by normalized action timestamps rather than token index, so the model natively represents inter-action timing (burstiness, mechanical cadence).
- Grouped-query attention, RMSNorm, and SwiGLU.
- Per-action features (action type, product surface, dwell time,
device/client signals, engagement-target hashes, and more), extracted from
raw Arrow IPC sequence bytes by the Rust
abuse-v3-featuresextractor and rewritten byruntime/feature_norm.pyto the training-time input dialect. - Eight task heads (
runtime/heads.py): FollowBot, LikeBot, EngagementAmplifier, ReplySpamBot, TweetSpamBot, RTBot, MultiActionBot, and LegitimateUser. Trained with class-balanced BCE plus a symmetric reverse-CE noise-robust term, with focal weighting wired through to the loss (runtime/loss.py).
The backbone is frozen at serving time. Task heads (runtime/task_heads.py)
are an MLP over the backbone CLS embedding, trained separately on cached
activations (training/train_head.py) and loaded by sha256 alongside the
backbone export (runtime/load_backbone.py). Weights ship as npz + per-array
sha256 in MANIFEST.json — no pickle deserialization surface.
- Backbone: self-supervised encoder over unlabeled action sequences
(masked-attribute prediction every step; a two-view InfoNCE contrastive
objective every 6th step). The public artifact is the exported
backbone.npz. - Task heads (
training/train_head.py): masked multi-label SCE over the eight heads, trained on cached CLS embeddings. Non-finite loss or gradient is a hard stop (never zero-and-continue). Focal gamma is a real parameter with an emitted stat, asserted present at step 0.
Kafka (user action events)
→ rust/accumulator dedup + cooldown, enqueue user_ids to Redis
→ runtime/batch_prefetcher fetch sequences (cache-first, store fallback),
Rust feature extraction,
publish GPU-ready batches to Kafka
→ runtime/gpu_scorer feature_norm, JAX inference on GPU,
publish 8-wide score rows
→ runtime/score_results_sink_focal
thresholds per head, dedup/cooldown ledgers,
graduated actioning (challenge vs. suspend),
BigQuery + protobuf event output
proto/abuse_inference.proto defines the events exchanged between stages
(ScoreRequest, ScoreResult, FiredHead, etc.).
The scorer publishes an 8-wide row in heads.HEAD_NAMES order.
- Python ≥ 3.10; see
pyproject.toml. GPU inference/training additionally requires JAX with CUDA,optax, and thehaiku2neural-network library. - The Rust feature extractor (
rust/abuse-v3-features) builds as a PyO3 extension module (abuse_v3_features). Serving pins the extractor that produced the training-time features; rebuild from this tree or supply a matching.so. - The pipeline reads action sequences from an internal key-value store
via a gRPC sidecar, and account metadata over HTTP. Those clients are
included for completeness but the backing services are not part of this
release; adapt the fetch layer
(
runtime/manhattan_scorer.py:ManhattanArrowReader) to your data source. - The prefetcher's sequence fetch is cache-first (
runtime/sequence_cache.py): a Redis GET against a historical sequence cache (enabled by default,--no-seq-cacheto disable) with fallback to the sequence store and a best-effort read-through fill, plus an optional realtime-action merge (--realtime-cache-enabled) that appends not-yet-flushed actions from a Redis sorted-set cache and re-sorts the sequence before feature extraction. Both caches are assumed to be reachable through local Envoy redis-proxy listeners; every cache failure degrades to the plain store path. - Generated protobuf bindings for the internal sequence-store schema are not
included; code paths that import
proto_gen.recsys_pb2degrade gracefully or require regenerating bindings for your own schema. - Hostnames, broker addresses, and project names in defaults are placeholders
(
localhost:9092,your-gcp-project) — override them via CLI flags or environment variables. Weight paths (--backbone-dir,--head-checkpoint) have no baked-in filesystem defaults. - Authoritative dimensions for the shipped configuration: 256 action types, 8 classification heads, sequence length 512, embedding width 1024.
- The results-sink enforcement operating points — the per-head decision
thresholds in
runtime/sink_policy.yaml(and the matching fallback defaults inscore_results_sink_focal.py) — are redacted in this public release: they ship as an out-of-range9.99sentinel (the fields are probabilities in[0, 1], so9.99never fires and is plainly a placeholder, not a real value). Publishing exact operating points would hand adversaries the detector's evasion boundary. The policy structure, head names, and gate logic are real and unredacted; only the tuned numbers are withheld. Supply your own via--policy-file/BDSM_SINK_POLICY. - Per-head appeal-note templates: the production sink interpolates a
short prose paragraph from the dominant bot head and selected histogram
counts (
build_enforcement_noteinruntime/score_results_sink_focal.py). The public package keeps the gates (MIN_ACTIONS, dominant-head pick) and theenforcement_noteproto field. The template strings and the per-headkey_actionsinterpolator are the sentinel"<redacted>"— same idea as the9.99operating points. When a note would have fired it carries that sentinel plus the model-head suffix, not the internal appeal paragraph or the action types each head keys off. ActionName proto enums are unchanged.