Agentic Credit Scoring with MoE Observability and Circuit Tracing
CreditScope is an AI-powered credit analysis platform built on Qwen3.5-35B-A3B-FP8, a 35-billion-parameter mixture-of-experts language model. It provides a full-stack application for credit scoring, real-time MoE expert routing visualization, chain-of-thought reasoning control, and mechanistic interpretability via circuit tracing with sparse autoencoders.
Key design decision: A single instance of the 35B model serves both chat inference and circuit tracing. The circuit tracer captures activations from the running SGLang server via forward hooks and filesystem-based IPC, avoiding the need to load a second copy of the model (which would require ~70GB additional VRAM).
Machine Setup from Scratch
These instructions provision a fresh Ubuntu server with GPU support for running CreditScope natively.
1. Hardware Requirements
Component
Minimum
Recommended
GPU
NVIDIA with 48GB+ VRAM
NVIDIA RTX PRO 6000 (96GB) or A100 80GB
CPU
8 cores
16+ cores
RAM
32GB
64GB+
Storage
100GB free
200GB+ (model weights ~35GB)
OS
Ubuntu 22.04 LTS
Ubuntu 24.04 LTS
For Blackwell-generation GPUs (RTX PRO 6000, sm_120), specific SGLang flags are required — see the SGLang flags section.
1# Required2AUTH_USERS=admin@creditscope.local # comma-separated allowed emails3AUTH_PASSWORD=your-secure-password # shared login password4AUTH_SECRET_KEY=$(openssl rand -hex 32)# session signing key56# Set your server's public IP for CORS7CORS_ORIGINS=http://localhost:3000,http://YOUR_PUBLIC_IP
89# Optional: HuggingFace token if model is gated10HUGGING_FACE_HUB_TOKEN=hf_xxxxxxxxxxxxx
5. Create Python Virtual Environment
bash
1python3 -m venv .venv
2source .venv/bin/activate
34# Install the project with all dependencies5pip install -e ".[dev,backend,inference,circuit]"
For Blackwell GPUs, you may need a specific CuDNN version:
DeltaNet layers use linear attention (O(n) vs O(n^2)), which makes the model efficient for long sequences. Standard attention layers every 4th position provide full-context mixing.
Mixture of Experts (MoE): Every layer has 256 experts but only routes each token to the top 8. This gives the model 35B total parameters but only ~3B active per token, enabling fast inference.
2. ReAct Agent Loop
The agent uses a Reason-Act-Observe loop to answer credit analysis queries:
User Query: "Evaluate John Smith for a $50,000 business loan"
│
▼
┌─────────────────────────────────────┐
│ 1. REASON (thinking tokens) │
│ "I need to check credit score, │
│ DTI ratio, and collateral..." │
│ │
│ 2. ACT (tool call) │
│ calculate_credit_score(id=42) │
│ │
│ 3. OBSERVE (tool result) │
│ Score: 720, Grade: B │
│ │
│ 4. REASON again │
│ "Score is good, need DTI next" │
│ │
│ 5. ACT │
│ calculate_dti(id=42, amount=50k)│
│ │
│ ... (up to 8 steps) ... │
│ │
│ FINAL: Synthesize response │
└─────────────────────────────────────┘
Available Credit Tools:
Tool
Description
calculate_credit_score
Weighted score from payment history (35%), utilization (30%), age (15%), mix (10%), inquiries (10%)
calculate_dti
Front-end and back-end debt-to-income ratios with risk classification
Regulatory and behavioral risk adjustments to base score
3. MoE Expert Routing Observability
During every inference request, forward hooks on the MoE gate modules capture:
For each of the 40 MoE layers:
┌─────────────────────────────────────────────┐
│ router_logits: [num_tokens × 256 experts] │
│ │ │
│ ▼ softmax + top-8 selection │
│ selected_experts: [num_tokens × 8] │
│ gating_weights: [num_tokens × 8] │
│ │
│ Metrics computed: │
│ - Expert load distribution │
│ - Shannon entropy of routing distribution │
│ - Per-expert activation frequency │
└─────────────────────────────────────────────┘
Shannon entropy measures routing diversity:
Low entropy → tokens concentrate on few experts (specialized)
High entropy → tokens spread evenly across experts (generic)
The frontend displays this as a real-time heatmap of expert activations across layers.
4. Circuit Tracing with Sparse Autoencoders
Circuit tracing discovers which internal features of the model drive a specific prediction. The pipeline has five stages:
Stage 1: Activation Capture
When a trace is requested, the backend creates a sentinel file (/tmp/circuit_trace_capture). Forward hooks registered in the SGLang process detect this and save the residual stream output from each decoder layer:
This filesystem-based IPC has zero overhead during normal chat — the hook checks one stat() call per layer and returns immediately if the sentinel doesn't exist.
Stage 2: Sparse Autoencoder Feature Extraction
Each SAE decomposes a 2048-dimensional residual stream vector into ~16,384 sparse features:
Residual stream x ∈ R^2048
│
▼
x_centered = x - bias
│
▼
pre_act = W_enc · x_centered + b_enc (2048 → 16384)
│
▼
z = JumpReLU(pre_act) Sparse: ~50 active out of 16384
│ z_i = pre_act_i if pre_act_i > θ_i
▼ z_i = 0 otherwise
x_hat = W_dec · z + bias (16384 → 2048)
JumpReLU activation (from Anthropic's scaling monosemanticity work) learns a per-feature threshold θ_i. Features only activate when their pre-activation exceeds this learned threshold, giving cleaner sparsity than standard ReLU.
Training objective:
L = ||x - x_hat||² + λ · ||z||₁
───────────── ──────────
reconstruction sparsity
loss penalty (λ = 3×10⁻⁴)
SAE Registry manages 68 SAEs across the full model:
40 language SAEs (one per decoder layer, 2048 → 16384 features)
27 vision SAEs (one per ViT layer, 1152 → 9216 features)
SAEs are created on-demand when a layer is first traced, avoiding allocating all 68 at startup.
Stage 3: Attribution Graph Construction
The graph represents causal flow from input tokens through features to the output prediction:
Nodes:
- Input: one per token position
- Feature: (layer, position, feature_idx) with activation value
- Output: the target token prediction
Edges:
- Feature → Output: activation value (how much this feature contributes)
- Feature → Feature: virtual weight × source activation
Virtual weights between features in adjacent layers are computed as:
This captures the linear pathway: how much a source feature's decoder direction projects onto the target feature's encoder direction.
Stage 4: Graph Pruning
Raw graphs can have thousands of nodes. Pruning keeps only high-impact nodes:
1. Score each node by backward-propagated importance:
- Output nodes get importance = 1.0
- For each edge (src → tgt):
importance[src] += |edge.weight| × importance[tgt]
2. Rank feature nodes by importance score
3. Keep top 10% (configurable), always keeping input/output nodes
4. Drop edges between pruned nodes
Stage 5: Feature Steering (Causal Validation)
Once a circuit is identified, steering validates whether those features actually control the output:
Baseline:
model("Analyze loan risk") → "The applicant shows moderate risk..."
Intervention (clamp feature 6392 at layer 39 to 0):
model("Analyze loan risk") → "The applicant appears to be..."
▲ different output confirms
feature 6392 was causal
Steering works by:
Running the model normally to get a baseline output
Registering a forward hook at the target layer that:
Encodes the residual stream through the SAE
Modifies the specified feature activation(s)
Decodes back to residual stream space
Generating again with the hook active
Comparing the outputs
5. Chain-of-Thought Budget Control
The thinking budget system controls how many tokens the model spends on internal reasoning before responding:
User query arrives
│
▼
┌───────────────────────────┐
│ Budget Resolution: │
│ "standard" → 2048 tkns │
│ "deep" → 32768 tkns│
│ "none" → 0 (skip) │
└───────────────────────────┘
│
▼
SGLang API call with:
max_completion_tokens = budget + response_limit
thinking { type: "enabled", budget_tokens: 2048 }
│
▼
Model generates:
<think>I need to evaluate... [up to budget tokens]</think>
The credit analysis shows... [response tokens]
The frontend displays thinking content in a collapsible panel with token count and duration.
API Reference
Authentication
POST /api/auth/login { email, password } → session cookie
GET /api/auth/me → current user info
Chat
POST /api/chat Process chat message with agent
WS /api/chat/ws WebSocket streaming (thinking + response deltas)
Customers
GET /api/customers List customers (paginated)
GET /api/customers/{id} Customer details
GET /api/customers/{id}/credit-report Full credit report
Circuit Tracer
POST /api/circuit/trace Trace a prompt → attribution graph
GET /api/circuit/architecture Model architecture map (lang + vision)
GET /api/circuit/saes List SAE checkpoints
GET /api/circuit/transcoders List transcoder checkpoints
GET /api/circuit/registry/status Registry summary (counts, config)
POST /api/circuit/steer Run feature steering intervention
Observability
GET /api/observability/moe/current Current MoE expert activations
GET /api/observability/moe/history Historical expert routing data
GET /api/observability/thinking/sessions Thinking session data
Configuration
GET /api/thinking/budgets Available thinking budget presets
POST /api/thinking/config Update thinking configuration
System
GET /health Health check
GET /metrics Prometheus metrics (text format)
Use a Hugging Face model repo for trained SAE and transcoder checkpoints. Do not publish trained weights to the dataset backup repo unless you intentionally want them stored as raw data artifacts.
The trained model files are written under:
circuit_tracer/data/checkpoints/
The collected activation datasets are written under:
1source .venv/bin/activate
23# Run tests4pytest
56# Lint7ruff check .89# Type check10mypy backend inference circuit_tracer
1112# Format13ruff format.
Running Without GPU
./scripts/run_dev.sh --no-inference
The backend and frontend will start without the inference server. Chat will be unavailable, but you can develop on the UI, credit tools, and database.
Production Hardening
Add a watchdog cron job for auto-restart:
* * * * * cd /home/ubuntu/creditscope && ./scripts/watchdog.sh
Environment variables for watchdog:
WATCHDOG_BACKEND_URL — defaults to http://127.0.0.1:8080/health
WATCHDOG_INFERENCE_URL — defaults to http://127.0.0.1:8000/model_info
WATCHDOG_RESTART_COOLDOWN_SECONDS — defaults to 120
Training SAEs and Transcoders from Scratch
This section describes how to collect fresh activations and train SAEs/TCs from zero — no pre-existing checkpoints or activations needed.
Overview
1. Collect activations ──→ 2. Train SAEs + TCs ──→ 3. Push to HF ──→ 4. Run app
(BF16 model + dataset) (from saved .npy) (checkpoints) (load checkpoints)
Step 1: Collect Activations
The collection script loads the BF16 model, runs forward passes on financial text, and captures the residual stream (pre and post) at each target layer.
Data source:sarel/creditscope-fino1-activations (HuggingFace dataset with text column of financial reasoning text).
Why BF16 and not FP8? The FP8 model (Qwen3.5-35B-A3B-FP8) does not load correctly via transformers — the weight_scale_inv dequantization tensors are ignored, producing corrupted weights. FP8 is just weight compression; the BF16 model produces nearly identical activations. If SGLang becomes available (fixes for sgl_kernel SM120), you can collect from FP8 via SGLang instead.
Step 2: Train SAEs and Transcoders
bash
1# Train all SAEs and TCs from the collected activations2python scripts/retrain_from_saved_activations.py
This script:
Loads activation chunks from circuit_tracer/data/activations/
Trains one JumpReLU SAE per layer (d_model=2048 → 16384 features)
Trains one MoE Transcoder per layer (maps pre → post activations)
Applies normalization when activation std > 1.0 (targets std=0.01)