po03087 commited on
Commit
4f3fc40
·
verified ·
1 Parent(s): 1047290

Add GAMEFORMER_SRA.md (negative result) + standalone GameFormer/C2F/SRA-GameFormer scripts

Browse files
GAMEFORMER_SRA.md ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GameFormer + SRA — a negative result
2
+
3
+ **Question.** SRA improves three *iterative denoisers* (MID, LED, MoFlow). Do its ideas also help
4
+ a **feedforward, non-diffusion** predictor? If so, SRA generalizes beyond denoising.
5
+
6
+ **Answer. No.** Injecting SRA's three components into GameFormer is **neutral-to-slightly-negative**.
7
+
8
+ | Model (NBA, min-ADE₂₀ / min-FDE₂₀ @ 4.0 s) | ADE₄ | FDE₄ | params |
9
+ |---|---|---|---|
10
+ | Vanilla GameFormer (control) | **0.8496** | **0.9557** | 11.85 M |
11
+ | GameFormer **+ SRA** (`GF_SRA=1`) | 0.8554 | 0.9564 | 11.90 M |
12
+ | **Δ (SRA − vanilla)** | **+0.0058 (worse)** | +0.0007 (≈neutral) | +0.05 M |
13
+
14
+ Both peaked at epoch 48 of 50. Same file, same seed (3407), same schedule — **only the three flags differ**.
15
+
16
+ ---
17
+
18
+ ## 1. Experimental design
19
+
20
+ The comparison is a **within-file ablation**, not a cross-codebase one:
21
+
22
+ * One script, `gameformer_sra_nba_standalone.py`, contains both arms.
23
+ * Three env-gated components; **with all flags off the code path is exactly vanilla GameFormer.**
24
+ * Verified: flags-off reports **11.85 M params** — identical to the separate vanilla
25
+ implementation, which scores 0.848/0.951. The refactor perturbs nothing.
26
+ * SRA adds only **+0.05 M params (+0.4 %)**, so any effect is attributable to the *mechanism*,
27
+ not to capacity.
28
+
29
+ Config: level-k = 3, K = 20 modes, 6 encoder layers, AdamW lr 1e-4, wd 1e-4, grad-clip 5,
30
+ MultiStepLR ×0.5, batch 128, 50 epochs, seed 3407. Standalone predictor — history only,
31
+ **no denoiser and no external future signal**.
32
+
33
+ ---
34
+
35
+ ## 2. What was injected
36
+
37
+ All three live inside GameFormer's `InteractionDecoder` (one per level-k stage).
38
+
39
+ ### 2.1 Uncertainty-gated message passing — `GF_SRA_SIGMA`
40
+
41
+ GameFormer's GMM head already *emits* a per-step log-σ but never **uses** it for interaction.
42
+ SRA turns it into a message gate: uncertain agents send weaker interaction messages.
43
+
44
+ ```python
45
+ sig = torch.exp(last_traj[..., 2:].clamp(0, 5)).mean(-1) # [B,K,A,T] from prev level's GMM
46
+ sig_agent = (sig * w[..., None]).sum(2).mean(-1) # [B,A] score-weighted, mean over T
47
+ ...
48
+ futures = futures * self.sigma_gate(sig_agent[..., None]) # MLP(1→32→1)+Sigmoid ⇒ gate ∈ (0,1)
49
+ ```
50
+
51
+ ### 2.2 Sparse RAG neighbor selection — `GF_SRA_TOPN`
52
+
53
+ GameFormer attends **densely** over all agents. SRA restricts each agent to its top-N (=5)
54
+ neighbours, chosen by *future* proximity and biased toward confident agents.
55
+
56
+ ```python
57
+ prox = (rep[:, :, None] - rep[:, None]).norm(dim=-1).mean(-1) # [B,A,A] mean future distance
58
+ if sig_agent is not None:
59
+ prox = prox + SIG_SEL_W * sig_agent[:, None, :] # prefer confident neighbours
60
+ prox = prox.masked_fill(eye[None], float('inf')) # exclude self
61
+ nbr_idx = prox.topk(N, largest=False, dim=-1).indices
62
+ ```
63
+
64
+ The resulting mask is applied **both** to the interaction self-attention (`attn_mask`) and to the
65
+ cross-attention key mask, so sparsity is enforced everywhere, not just in one place.
66
+
67
+ ### 2.3 Explicit relational future features — `GF_SRA_RELFUT`
68
+
69
+ GameFormer's `FutureEncoder` encodes each agent's future **individually**. SRA adds explicit
70
+ **pairwise-relative** future geometry (Δposition, Δvelocity), max-pooled over time.
71
+
72
+ ```python
73
+ dpos = rep[:, :, None] - rep[:, None] # [B,A,A,T,2]
74
+ dvel = F.pad(torch.diff(dpos, dim=-2), (0, 0, 1, 0))
75
+ relf = self.rel_mlp(torch.cat([dpos, dvel], -1)).max(dim=-2).values # MLP(4→64→D)
76
+ futures = futures + relf.sum(2) / denom # neighbour-mean
77
+ ```
78
+
79
+ ---
80
+
81
+ ## 3. Reproducing
82
+
83
+ ```bash
84
+ # control (vanilla GameFormer)
85
+ python gameformer_sra_nba_standalone.py --gpu 0 --exp van --level 3 --epochs 50 --batch_size 128
86
+
87
+ # + SRA (all three components)
88
+ GF_SRA=1 python gameformer_sra_nba_standalone.py --gpu 0 --exp sra --level 3 --epochs 50 --batch_size 128
89
+ ```
90
+
91
+ | Variable | Effect |
92
+ |---|---|
93
+ | `GF_SRA=1` | enable all three components |
94
+ | `GF_SRA_SIGMA` / `GF_SRA_TOPN` / `GF_SRA_RELFUT` | enable individually |
95
+ | `GF_TOPN` | neighbour budget (default 5) |
96
+ | `GF_SIG_SEL_W` | weight of σ in neighbour selection (default 0.5) |
97
+
98
+ The script prints its configuration so every run is self-documenting:
99
+
100
+ ```
101
+ [SRA-GF] SRA components: neighbor-topN=True(N=5) sigma-gate=True relational-future=True
102
+ ```
103
+
104
+ ---
105
+
106
+ ## 4. Why it does not transfer
107
+
108
+ 1. **The future-interaction idea is already native to GameFormer.** Its level-k reasoning
109
+ *is* "condition each agent on the others' predicted futures". SRA's relational features
110
+ are therefore largely **redundant** here, adding parameters and noise rather than new signal.
111
+ In MID/LED/MoFlow nothing plays this role, which is exactly why SRA helps there.
112
+
113
+ 2. **The uncertainty signal is weaker.** In a denoiser, σ is *principled* — a noise schedule or
114
+ the leapfrog initializer's variance estimate, tied to how much the model should trust its
115
+ current iterate. GameFormer's log-σ is just a learned regression head; gating on it does not
116
+ carry comparable information.
117
+
118
+ 3. **Sparsity costs more than it saves at A = 11.** Dense attention over 11 agents is cheap and
119
+ fully informative; discarding all but 5 neighbours removes usable context. SRA's sparse
120
+ selection pays off on 23-agent sport scenes and on hosts where dense interaction is unstable.
121
+
122
+ 4. **One-shot vs iterative (the core reason).** SRA is built to *refine an evolving estimate*: it
123
+ is applied at every denoising step, so its gated residual is corrected repeatedly and errors
124
+ are damped. A feedforward decoder gets **one** pass — no iterative correction, no damping.
125
+ The regime SRA was designed for is absent.
126
+
127
+ ---
128
+
129
+ ## 5. How to read this result
130
+
131
+ This is a **scope finding, not a failure**, and it is consistent with the paper's framing:
132
+ SRA is a *denoiser* adapter. Its gains come from repeatedly reshaping an iteratively refined
133
+ future estimate — a mechanism a single-pass predictor does not have.
134
+
135
+ Stated honestly:
136
+
137
+ > SRA's gains are specific to the iterative-denoising setting. Applied to a strong feedforward
138
+ > predictor (GameFormer), it is neutral-to-slightly-negative, which supports rather than
139
+ > contradicts the paper's positioning.
140
+
141
+ It mirrors the MoFlow observation from the other direction: the *host's* update mechanism
142
+ determines what an interaction module can contribute.
143
+
144
+ **Caveats.** Single seed; the ADE gap (0.006, 0.7 %) is small enough that multi-seed runs could
145
+ place it within noise — i.e. "neutral" rather than "worse". It is clearly **not** an improvement
146
+ either way. No per-component ablation was run, so it remains open whether one component alone
147
+ transfers even though the combination does not.
README.md CHANGED
@@ -18,6 +18,12 @@ It covers the required directory layout, environment setup, the exact training c
18
  host × dataset, the E2 ablation settings, environment-variable switches, the adapter contract,
19
  and known gotchas.
20
 
 
 
 
 
 
 
21
  ## Contents
22
 
23
  ```
 
18
  host × dataset, the E2 ablation settings, environment-variable switches, the adapter contract,
19
  and known gotchas.
20
 
21
+ ## Documents
22
+
23
+ - [`RUNNING.md`](RUNNING.md) — how to run every host × dataset
24
+ - [`GAMEFORMER_SRA.md`](GAMEFORMER_SRA.md) — GameFormer+SRA negative result (does SRA generalize to feedforward models?)
25
+ - [`sample_data/README.md`](sample_data/README.md) — bundled 100-scene NBA smoke-test subset
26
+
27
  ## Contents
28
 
29
  ```
standalone/c2f_nba_standalone.py ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ c2f_nba_standalone.py
5
+ ================================================================================
6
+ FAITHFUL standalone re-implementation of the **Coarse-to-Fine** trajectory
7
+ predictor of ref [22] ("Towards Capturing the Temporal Dynamics for Trajectory
8
+ Prediction: A Coarse-to-Fine Approach") as a STANDALONE predictor for the NBA
9
+ basketball dataset.
10
+
11
+ It is a STANDALONE predictor: it consumes ONLY agent HISTORY (past trajectories)
12
+ and predicts K multi-modal futures via a native COARSE stage followed by an
13
+ autoregressive temporal FINE-refinement stage. It does NOT consume any external
14
+ denoiser / diffusion prediction (unlike the plug-in `CoarseToFineRefine` module,
15
+ which was handed the host's intermediate future estimate -> that borrowed SRA's
16
+ future-interaction signal and was therefore unfair; this standalone version
17
+ generates its own coarse trajectory, so it is faithful to the native method).
18
+
19
+ --------------------------------------------------------------------------------
20
+ WHAT THE NATIVE METHOD IS (and what we replicate)
21
+ --------------------------------------------------------------------------------
22
+ Coarse-to-fine = two-stage decoding that *captures temporal dynamics* by first
23
+ predicting a rough full trajectory and then refining it step-by-step:
24
+
25
+ STAGE 0 (COARSE): a multimodal decoder emits K rough full-horizon
26
+ trajectories per agent from the social-encoded context (mode + agent query
27
+ cross-attended to all agents' context, then an MLP trajectory head). This is
28
+ the model's OWN coarse prediction -- it is NOT received from a host.
29
+
30
+ STAGE 1..S (FINE): the coarse trajectory is walked TEMPORALLY by a
31
+ unidirectional (autoregressive) GRU -- the mechanism the paper uses to
32
+ capture temporal dynamics -- conditioned on the mode's context, emitting a
33
+ per-timestep residual correction delta_t ; y_fine = y_coarse + delta.
34
+ Repeated S times (progressive coarse -> fine -> finer). delta head is
35
+ zero-initialised so training first fits the coarse stage, then the refiner
36
+ engages (stable).
37
+
38
+ Mode scores: a per-mode classification head; winner-takes-all training.
39
+
40
+ ENCODER (shared, IDENTICAL to the GameFormer standalone so the E4 comparison
41
+ isolates the DECODER mechanism, coarse-to-fine vs level-k, not the backbone):
42
+ * AgentHistoryEncoder = 2-layer LSTM(6->256) over agent history + learned
43
+ player/ball type embedding.
44
+ * FusionEncoder = nn.TransformerEncoder (d=256, heads=8, ff=1024, gelu),
45
+ `encoder_layers` deep -> agent<->agent social self-attention (no map: NBA).
46
+
47
+ LOSS (native coarse-to-fine supervision):
48
+ * coarse WTA-L2 (variety loss over K modes) + fine WTA-L2 + mode
49
+ cross-entropy (label smoothing 0.2) on the fine-stage winning mode.
50
+ * endpoint-emphasised distance (mean_t + checkpoint-step sum) for mode
51
+ selection, marginal PER AGENT (NBA Table-1 metric is marginal min-ADE_20).
52
+
53
+ --------------------------------------------------------------------------------
54
+ NBA I/O + METRIC (matched to MoFlow / Table 1 -- identical to GameFormer standalone)
55
+ --------------------------------------------------------------------------------
56
+ * Data: MoFlow's data/dataloader_nba.py::NBADatasetMinMax, same .npy, split,
57
+ scaling (traj_scale=94/28, traj_mean=[14,7.5]). Past=10, Future=20 (4.0 s).
58
+ * Predict in centered-abs frame (pos/scale - mean); cur_xy = last past step;
59
+ gt_center = fut_traj_original_scale (displacement) + cur_xy.
60
+ * Metric (identical to eval_perscene_moflow.py):
61
+ d = ||pred_disp - gt_disp|| ; ADE4 = d[:,:20].mean_t.min_K ; FDE4 =
62
+ d[:,19].min_K ; mean over 11 agents & scenes (marginal min-of-K=20 @ 4s).
63
+
64
+ Prints: [C2F-NBA] epoch N ADE4=.. FDE4=..
65
+ """
66
+
67
+ import os
68
+ import sys
69
+ import argparse
70
+ import time
71
+
72
+ # ------------------------------------------------------------------ GPU FIRST
73
+ def _early_gpu():
74
+ for i, a in enumerate(sys.argv):
75
+ if a == '--gpu' and i + 1 < len(sys.argv):
76
+ return sys.argv[i + 1]
77
+ if a.startswith('--gpu='):
78
+ return a.split('=', 1)[1]
79
+ return None
80
+
81
+
82
+ _g = _early_gpu()
83
+ if _g is not None:
84
+ os.environ['CUDA_VISIBLE_DEVICES'] = str(_g)
85
+ os.environ.setdefault('MPLBACKEND', 'Agg')
86
+
87
+ # ------------------------------------------------ reuse MoFlow's NBA pipeline
88
+ MOFLOW_ROOT = '/mnt/jaewoo4tb/srtp/MoFlow'
89
+ if MOFLOW_ROOT not in sys.path:
90
+ sys.path.insert(0, MOFLOW_ROOT)
91
+
92
+ import types as _types
93
+ import numpy as np
94
+ import torch
95
+ import torch.nn as nn
96
+ import torch.nn.functional as F
97
+ from torch.utils.data import DataLoader
98
+
99
+ try:
100
+ os.chdir(MOFLOW_ROOT)
101
+ except Exception:
102
+ pass
103
+ from data.dataloader_nba import NBADatasetMinMax, seq_collate_nba
104
+
105
+
106
+ # ============================================================================
107
+ # PRIMITIVES (shared with the GameFormer standalone, verbatim)
108
+ # ============================================================================
109
+ D_MODEL = 256
110
+ N_HEADS = 8
111
+ DROPOUT = 0.1
112
+
113
+
114
+ class CrossTransformer(nn.Module):
115
+ def __init__(self, dim=D_MODEL, heads=N_HEADS, dropout=DROPOUT):
116
+ super().__init__()
117
+ self.cross_attention = nn.MultiheadAttention(dim, heads, dropout, batch_first=True)
118
+ self.norm_1 = nn.LayerNorm(dim)
119
+ self.norm_2 = nn.LayerNorm(dim)
120
+ self.ffn = nn.Sequential(
121
+ nn.Linear(dim, dim * 4), nn.GELU(), nn.Dropout(dropout),
122
+ nn.Linear(dim * 4, dim), nn.Dropout(dropout))
123
+
124
+ def forward(self, query, key, value, mask=None):
125
+ attn, _ = self.cross_attention(query, key, value, key_padding_mask=mask)
126
+ attn = self.norm_1(attn)
127
+ return self.norm_2(self.ffn(attn) + attn)
128
+
129
+
130
+ class AgentHistoryEncoder(nn.Module):
131
+ """2-layer LSTM(6->256) over agent history + player/ball type embedding."""
132
+
133
+ def __init__(self, in_dim=6, dim=D_MODEL, n_types=2):
134
+ super().__init__()
135
+ self.motion = nn.LSTM(in_dim, dim, 2, batch_first=True)
136
+ self.type_emb = nn.Embedding(n_types, dim)
137
+
138
+ def forward(self, hist, types):
139
+ B, A, T, C = hist.shape
140
+ traj, _ = self.motion(hist.reshape(B * A, T, C))
141
+ out = traj[:, -1].reshape(B, A, -1)
142
+ out = out + self.type_emb(types)[None]
143
+ return out
144
+
145
+
146
+ class FusionEncoder(nn.Module):
147
+ """Agent<->agent social self-attention (no map for NBA)."""
148
+
149
+ def __init__(self, dim=D_MODEL, heads=N_HEADS, layers=6, dropout=DROPOUT):
150
+ super().__init__()
151
+ layer = nn.TransformerEncoderLayer(
152
+ d_model=dim, nhead=heads, dim_feedforward=dim * 4,
153
+ activation=F.gelu, dropout=dropout, batch_first=True)
154
+ self.encoder = nn.TransformerEncoder(layer, layers, enable_nested_tensor=False)
155
+
156
+ def forward(self, tokens, mask=None):
157
+ return self.encoder(tokens, src_key_padding_mask=mask)
158
+
159
+
160
+ # ============================================================================
161
+ # COARSE-TO-FINE DECODER
162
+ # ============================================================================
163
+ class CoarseDecoder(nn.Module):
164
+ """Stage-0: K multimodal rough full-horizon trajectories per agent.
165
+ Mode + agent query added to the agent's context token, cross-attended to the
166
+ full agent context (social), then an MLP trajectory head + a mode-score head."""
167
+
168
+ def __init__(self, modalities, n_agents, future_len, dim=D_MODEL):
169
+ super().__init__()
170
+ self.M = modalities
171
+ self.multi_modal_query_embedding = nn.Embedding(modalities, dim)
172
+ self.agent_query_embedding = nn.Embedding(n_agents, dim)
173
+ self.query_encoder = CrossTransformer(dim)
174
+ self.traj_head = nn.Sequential(
175
+ nn.Linear(dim, 512), nn.ELU(), nn.Dropout(0.1),
176
+ nn.Linear(512, future_len * 2))
177
+ self.score_head = nn.Sequential(
178
+ nn.Linear(dim, 64), nn.ELU(), nn.Dropout(0.1), nn.Linear(64, 1))
179
+ self.future_len = future_len
180
+ self.register_buffer('modal', torch.arange(modalities).long())
181
+ self.register_buffer('agent', torch.arange(n_agents).long())
182
+
183
+ def forward(self, encoding, cur_xy, mask=None):
184
+ B, A, D = encoding.shape
185
+ M, T = self.M, self.future_len
186
+ mm = self.multi_modal_query_embedding(self.modal) # [M, D]
187
+ ag = self.agent_query_embedding(self.agent) # [A, D]
188
+ query = encoding[:, :, None, :] + mm[None, None] + ag[None, :, None] # [B,A,M,D]
189
+
190
+ q = query.reshape(B * A, M, D)
191
+ kv = encoding[:, None, :, :].expand(B, A, A, D).reshape(B * A, A, D)
192
+ km = None
193
+ if mask is not None:
194
+ km = mask[:, None, :].expand(B, A, A).reshape(B * A, A)
195
+ content = self.query_encoder(q, kv, kv, km) # [B*A, M, D]
196
+
197
+ coarse = self.traj_head(content).view(B, A, M, T, 2)
198
+ coarse = coarse + cur_xy[:, :, None, None, :] # centered-abs
199
+ score = self.score_head(content).view(B, A, M)
200
+ return content.view(B, A, M, D), coarse, score
201
+
202
+
203
+ class FineRefiner(nn.Module):
204
+ """Stage-1..S: autoregressive temporal refinement of the coarse trajectory.
205
+ A unidirectional GRU walks the (centered) coarse trajectory, conditioned on
206
+ the mode context, emitting a per-timestep residual delta_t. Repeated S times."""
207
+
208
+ def __init__(self, dim=D_MODEL, hidden=256, n_stages=2, dropout=DROPOUT):
209
+ super().__init__()
210
+ self.n_stages = n_stages
211
+ self.pos_emb = nn.Linear(2, hidden)
212
+ self.ctx_proj = nn.Linear(dim, hidden)
213
+ self.gru = nn.GRU(hidden, hidden, 2, batch_first=True, dropout=dropout)
214
+ self.delta = nn.Linear(hidden, 2)
215
+ nn.init.zeros_(self.delta.weight) # start as identity: fine == coarse at init
216
+ nn.init.zeros_(self.delta.bias)
217
+
218
+ def forward(self, coarse, content, cur_xy):
219
+ # coarse:[B,A,M,T,2] content:[B,A,M,D] cur_xy:[B,A,2]
220
+ B, A, M, T, _ = coarse.shape
221
+ ctx = self.ctx_proj(content).reshape(B * A * M, 1, -1) # [N,1,H]
222
+ y = coarse
223
+ for _ in range(self.n_stages):
224
+ yc = (y - cur_xy[:, :, None, None, :]).reshape(B * A * M, T, 2) # center
225
+ seq = self.pos_emb(yc) + ctx # broadcast ctx over T
226
+ h, _ = self.gru(seq) # [N,T,H] autoregressive
227
+ d = self.delta(h).view(B, A, M, T, 2)
228
+ y = y + d
229
+ return y
230
+
231
+
232
+ class CoarseToFineNBA(nn.Module):
233
+ """Standalone coarse-to-fine predictor (NBA, map dropped)."""
234
+
235
+ def __init__(self, n_agents=11, past_dim=6, future_len=20, modalities=20,
236
+ n_stages=2, dim=D_MODEL, heads=N_HEADS, enc_layers=6, hidden=256,
237
+ n_types=2, ball_idx=10):
238
+ super().__init__()
239
+ self.history_encoder = AgentHistoryEncoder(past_dim, dim, n_types)
240
+ self.fusion_encoder = FusionEncoder(dim, heads, enc_layers)
241
+ self.coarse_decoder = CoarseDecoder(modalities, n_agents, future_len, dim)
242
+ self.fine_refiner = FineRefiner(dim, hidden, n_stages)
243
+ types = torch.zeros(n_agents, dtype=torch.long)
244
+ if 0 <= ball_idx < n_agents and n_types > 1:
245
+ types[ball_idx] = 1
246
+ self.register_buffer('agent_types', types)
247
+
248
+ def forward(self, feats, cur_xy):
249
+ enc = self.history_encoder(feats, self.agent_types) # [B,A,D]
250
+ enc = self.fusion_encoder(enc, None) # [B,A,D] social
251
+ content, coarse, score = self.coarse_decoder(enc, cur_xy, None)
252
+ fine = self.fine_refiner(coarse, content, cur_xy) # [B,A,M,T,2]
253
+ return {'coarse': coarse, 'fine': fine, 'scores': score}
254
+
255
+
256
+ # ============================================================================
257
+ # LOSS (coarse WTA + fine WTA + mode CE; marginal per agent)
258
+ # ============================================================================
259
+ def wta_l2(pred, gt, metric_idx):
260
+ """Winner-takes-all L2 (variety loss). pred:[B,A,M,T,2] gt:[B,A,T,2].
261
+ Mode selection uses endpoint-emphasised distance; returns best mode's mean
262
+ displacement (reconstruction loss) and the winning mode index [B,A]."""
263
+ B, A, M, T, _ = pred.shape
264
+ d = torch.norm(pred - gt[:, :, None], dim=-1) # [B,A,M,T]
265
+ sel = d.mean(-1) + d[..., metric_idx].sum(-1) # [B,A,M]
266
+ best = sel.argmin(-1) # [B,A]
267
+ gi = best[..., None, None, None].expand(B, A, 1, T, 2)
268
+ best_d = torch.norm(torch.gather(pred, 2, gi).squeeze(2) - gt, dim=-1) # [B,A,T]
269
+ reg = best_d.mean(-1) + best_d[..., metric_idx].sum(-1) # [B,A] endpoint emphasis
270
+ return reg.mean(), best
271
+
272
+
273
+ def c2f_loss(out, gt_center, metric_idx):
274
+ coarse_reg, _ = wta_l2(out['coarse'], gt_center, metric_idx)
275
+ fine_reg, best = wta_l2(out['fine'], gt_center, metric_idx)
276
+ B, A, M = out['scores'].shape
277
+ cls = F.cross_entropy(out['scores'].reshape(B * A, M), best.reshape(B * A),
278
+ label_smoothing=0.2)
279
+ return coarse_reg + fine_reg + 2.0 * cls
280
+
281
+
282
+ # ============================================================================
283
+ # DATA (identical to the GameFormer standalone)
284
+ # ============================================================================
285
+ def build_loaders(args):
286
+ cfg = _types.SimpleNamespace(
287
+ traj_mean=[14, 7.5], data_norm='min_max',
288
+ past_frames=args.past_len, future_frames=args.future_len, agents=args.agents)
289
+ train_set = NBADatasetMinMax(
290
+ obs_len=args.past_len, pred_len=args.future_len, training=True,
291
+ num_scenes=args.n_train, cfg=cfg, data_dir=args.data_dir,
292
+ rotate=False, data_norm='min_max')
293
+ test_set = NBADatasetMinMax(
294
+ obs_len=args.past_len, pred_len=args.future_len, training=False,
295
+ test_scenes=args.n_test, cfg=cfg, data_dir=args.data_dir,
296
+ rotate=False, data_norm='min_max')
297
+ train_loader = DataLoader(
298
+ train_set, batch_size=args.batch_size, shuffle=True, num_workers=args.workers,
299
+ collate_fn=seq_collate_nba, pin_memory=True, drop_last=True)
300
+ test_loader = DataLoader(
301
+ test_set, batch_size=args.test_batch, shuffle=False, num_workers=args.workers,
302
+ collate_fn=seq_collate_nba, pin_memory=True)
303
+ return train_loader, test_loader
304
+
305
+
306
+ def unpack(data, device):
307
+ feats = data['past_traj'].to(device) # [B,A,T,6] normalized
308
+ past_orig = data['past_traj_original_scale'].to(device) # [B,A,T,6] metric
309
+ gt_disp = data['fut_traj_original_scale'].to(device) # [B,A,Tf,2] displacement
310
+ cur_xy = past_orig[:, :, -1, 0:2]
311
+ gt_center = gt_disp + cur_xy[:, :, None, :]
312
+ return feats, cur_xy, gt_disp, gt_center
313
+
314
+
315
+ # ============================================================================
316
+ # EVAL (marginal min-of-K=20 at 4.0 s; identical to eval_perscene_moflow.py)
317
+ # ============================================================================
318
+ @torch.no_grad()
319
+ def evaluate(model, loader, device, end):
320
+ model.eval()
321
+ ade_sum = fde_sum = 0.0
322
+ n = 0
323
+ for data in loader:
324
+ feats, cur_xy, gt_disp, _ = unpack(data, device)
325
+ out = model(feats, cur_xy)
326
+ pred = out['fine'][..., :2] # [B,A,M,T,2] fine stage
327
+ pred_disp = pred - cur_xy[:, :, None, None, :]
328
+ d = torch.norm(pred_disp - gt_disp[:, :, None], dim=-1) # [B,A,M,T]
329
+ ade = d[..., :end].mean(-1).min(dim=-1).values # [B,A]
330
+ fde = d[..., end - 1].min(dim=-1).values # [B,A]
331
+ ade_sum += ade.sum().item()
332
+ fde_sum += fde.sum().item()
333
+ n += ade.numel()
334
+ return ade_sum / max(n, 1), fde_sum / max(n, 1)
335
+
336
+
337
+ # ============================================================================
338
+ # TRAIN
339
+ # ============================================================================
340
+ def parse_args():
341
+ p = argparse.ArgumentParser('Coarse-to-Fine standalone predictor for NBA')
342
+ p.add_argument('--data_dir', default='/mnt/jaewoo4tb/srtp/MoFlow/data/nba', type=str)
343
+ p.add_argument('--gpu', default='0', type=str)
344
+ p.add_argument('--exp', default='c2f_nba', type=str)
345
+ p.add_argument('--epochs', default=50, type=int)
346
+ p.add_argument('--batch_size', default=128, type=int)
347
+ p.add_argument('--test_batch', default=500, type=int)
348
+ p.add_argument('--workers', default=4, type=int)
349
+ p.add_argument('--seed', default=3407, type=int)
350
+
351
+ p.add_argument('--modalities', default=20, type=int, help='K modes (NBA min-ADE_20)')
352
+ p.add_argument('--stages', default=2, type=int, help='coarse->fine refinement stages')
353
+ p.add_argument('--encoder_layers', default=6, type=int)
354
+ p.add_argument('--hidden', default=256, type=int, help='fine-refiner GRU hidden')
355
+ p.add_argument('--agents', default=11, type=int)
356
+ p.add_argument('--past_len', default=10, type=int)
357
+ p.add_argument('--future_len', default=20, type=int)
358
+ p.add_argument('--ball_idx', default=10, type=int, help='-1 to disable type emb')
359
+
360
+ p.add_argument('--lr', default=1e-4, type=float)
361
+ p.add_argument('--weight_decay', default=1e-4, type=float)
362
+ p.add_argument('--grad_clip', default=5.0, type=float)
363
+ p.add_argument('--n_train', default=32500, type=int)
364
+ p.add_argument('--n_test', default=12500, type=int)
365
+ p.add_argument('--eval_every', default=2, type=int)
366
+ p.add_argument('--save_dir', default=os.path.join(
367
+ os.path.dirname(os.path.abspath(__file__)), 'c2f_nba_ckpt'), type=str)
368
+ return p.parse_args()
369
+
370
+
371
+ def main():
372
+ args = parse_args()
373
+ torch.manual_seed(args.seed)
374
+ np.random.seed(args.seed)
375
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
376
+
377
+ train_loader, test_loader = build_loaders(args)
378
+
379
+ n_types = 2 if (0 <= args.ball_idx < args.agents) else 1
380
+ model = CoarseToFineNBA(
381
+ n_agents=args.agents, past_dim=6, future_len=args.future_len,
382
+ modalities=args.modalities, n_stages=args.stages, dim=D_MODEL, heads=N_HEADS,
383
+ enc_layers=args.encoder_layers, hidden=args.hidden, n_types=n_types,
384
+ ball_idx=args.ball_idx).to(device)
385
+ n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
386
+ print(f'[C2F-NBA] model params: {n_params/1e6:.2f} M | stages={args.stages} '
387
+ f'K={args.modalities} enc_layers={args.encoder_layers} device={device}', flush=True)
388
+
389
+ opt = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
390
+ milestones = sorted(set(int(args.epochs * f) for f in (0.5, 0.65, 0.8, 0.9)))
391
+ milestones = [m for m in milestones if 0 < m < args.epochs]
392
+ sched = torch.optim.lr_scheduler.MultiStepLR(opt, milestones=milestones, gamma=0.5)
393
+
394
+ end = args.future_len
395
+ metric_idx = sorted(set([max(0, args.future_len // 2 - 1), args.future_len - 1]))
396
+
397
+ os.makedirs(args.save_dir, exist_ok=True)
398
+ best_ade = float('inf')
399
+
400
+ for epoch in range(args.epochs):
401
+ model.train()
402
+ t0 = time.time()
403
+ running = 0.0
404
+ nb = 0
405
+ for data in train_loader:
406
+ feats, cur_xy, _, gt_center = unpack(data, device)
407
+ out = model(feats, cur_xy)
408
+ loss = c2f_loss(out, gt_center, metric_idx)
409
+ opt.zero_grad()
410
+ loss.backward()
411
+ torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
412
+ opt.step()
413
+ running += float(loss.item())
414
+ nb += 1
415
+ sched.step()
416
+ print(f'[C2F-NBA] epoch {epoch+1}/{args.epochs} loss={running/max(nb,1):.4f} '
417
+ f'lr={opt.param_groups[0]["lr"]:.2e} ({time.time()-t0:.1f}s)', flush=True)
418
+
419
+ if (epoch + 1) % args.eval_every == 0 or epoch == args.epochs - 1:
420
+ ade, fde = evaluate(model, test_loader, device, end)
421
+ print(f'[C2F-NBA] epoch {epoch+1} ADE4={ade:.4f} FDE4={fde:.4f}', flush=True)
422
+ if ade < best_ade:
423
+ best_ade = ade
424
+ torch.save({'model': model.state_dict(), 'epoch': epoch + 1,
425
+ 'ade4': ade, 'fde4': fde, 'args': vars(args)},
426
+ os.path.join(args.save_dir, f'{args.exp}_best.pt'))
427
+ print(f'[C2F-NBA] saved best (ADE4={ade:.4f}) -> '
428
+ f'{os.path.join(args.save_dir, args.exp + "_best.pt")}', flush=True)
429
+
430
+ print(f'[C2F-NBA] done. best ADE4={best_ade:.4f}', flush=True)
431
+
432
+
433
+ if __name__ == '__main__':
434
+ main()
standalone/gameformer_nba_standalone.py ADDED
@@ -0,0 +1,614 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ gameformer_nba_standalone.py
5
+ ================================================================================
6
+ FAITHFUL standalone re-implementation of **GameFormer** (Liu et al., ICCV 2023,
7
+ "GameFormer: Game-theoretic Modeling and Learning of Transformer-based
8
+ Interactive Prediction", github.com/MCZhi/GameFormer) as a trajectory predictor
9
+ for the NBA basketball dataset.
10
+
11
+ It is a STANDALONE predictor: it consumes ONLY agent HISTORY (past trajectories)
12
+ and predicts K multi-modal futures via the level-k game-theoretic decoder. It
13
+ does NOT consume any external denoiser / diffusion prediction.
14
+
15
+ --------------------------------------------------------------------------------
16
+ WHAT WAS REPLICATED (evidence = MCZhi/GameFormer @ main)
17
+ --------------------------------------------------------------------------------
18
+ Reference files studied verbatim:
19
+ * model/GameFormer.py (Encoder, Decoder, GameFormer)
20
+ * model/modules.py (AgentEncoder, FutureEncoder, GMMPredictor,
21
+ SelfTransformer, CrossTransformer,
22
+ InitialDecoder, InteractionDecoder)
23
+ * utils/inter_pred_utils.py (level_k_loss, gmm_loss)
24
+ * interaction_prediction/train.py (AdamW 1e-4, MultiStepLR, grad-clip 5,
25
+ level=3, modalities=6, encoder_layers=6)
26
+
27
+ ENCODER (GameFormer.Encoder + modules.AgentEncoder)
28
+ * Per-agent temporal encoder = 2-layer LSTM, hidden 256 (nn.LSTM(8,256,2)).
29
+ We adapt the input feature dim 8 -> 6 (NBA has [abs xy, rel xy, vel xy], no
30
+ heading/size channels). A learned agent-type embedding (player vs. ball) is
31
+ added to the LSTM last-step output, exactly as GameFormer adds `type_emb`.
32
+ * Interaction / self-attention over agents = the GameFormer `fusion_encoder`,
33
+ a nn.TransformerEncoder of TransformerEncoderLayer(d_model=256, nhead=8,
34
+ dim_feedforward=1024, activation=gelu, dropout=0.1, batch_first=True),
35
+ `encoder_layers` deep. In GameFormer this fuses agent tokens WITH per-agent
36
+ map (lane/crosswalk) tokens; NBA has NO map, so the map encoders
37
+ (LaneEncoder / CrosswalkEncoder / segment_map) are DROPPED and the fusion is
38
+ pure agent<->agent self-attention over the 11 tokens (identical for every
39
+ target agent when there is no per-agent map, so it is computed once).
40
+
41
+ LEVEL-k GAME-THEORETIC DECODER (GameFormer.Decoder)
42
+ * Level 0 = InitialDecoder: for each agent, a set of `modalities` (=K) learned
43
+ modal query embeddings + a per-agent query embedding are added to the agent's
44
+ own context token, then cross-attended (CrossTransformer) to the full agent
45
+ context. A GMMPredictor head outputs K trajectories of (mu_x,mu_y,
46
+ log_sig_x,log_sig_y) per step + K mode scores. predictions[...,:2] += the
47
+ agent's current (last observed) position.
48
+ * Levels 1..L = InteractionDecoder (one per level, sharing ONE FutureEncoder):
49
+ - FutureEncoder encodes EVERY agent's K level-(k-1) modal futures into a
50
+ vector (max-pool over time of an MLP on [x,y,heading,vx,vy]); GameFormer
51
+ also appends box size (w,l,h) -> we drop those (no size in NBA), 8->5.
52
+ - The per-mode future encodings are score-softmax-weighted and mean-pooled
53
+ to one vector per agent, then passed through a SelfTransformer to produce
54
+ the interaction encoding over all agents (this is the game-theoretic
55
+ "condition on all other agents' level-(k-1) predictions" step).
56
+ - The interaction tokens are concatenated with the context tokens; each
57
+ agent's OWN future token is masked out; the previous-level content plus
58
+ the agent's own future encoding form the query, cross-attended to that
59
+ combined context, and a fresh GMMPredictor emits the level-k K futures.
60
+ * Output dict: level_0_interactions/scores ... level_L_interactions/scores,
61
+ exactly as GameFormer.Decoder.forward.
62
+
63
+ LOSS (utils.inter_pred_utils.level_k_loss + gmm_loss, gmm=True)
64
+ * Summed over ALL levels k=0..L (level-wise imitation, GameFormer's key term).
65
+ * Per level, GMM winner-takes-all: best mode = argmin over K of
66
+ (mean_t ||traj-gt|| + sum over checkpoint steps), then the Gaussian NLL
67
+ reg = (log_sig_x+log_sig_y) + 0.5*(dx^2/sx^2 + dy^2/sy^2) with log_sig
68
+ clipped to [0,5], reduced as mean_t + sum over checkpoints; plus mode
69
+ classification F.cross_entropy(scores, best_mode, label_smoothing=0.2)
70
+ weighted x2. We MARGINALIZE per agent (GameFormer's interaction_prediction
71
+ task predicts 2 agents JOINTLY via distance.mean(1)/scores.sum(1); the NBA
72
+ Table-1 metric is MARGINAL min-ADE_20, so the best mode / scores are taken
73
+ per agent instead of jointly -- the only loss adaptation).
74
+
75
+ --------------------------------------------------------------------------------
76
+ NBA I/O + METRIC (matched to MoFlow / Table 1)
77
+ --------------------------------------------------------------------------------
78
+ * Data: reuses MoFlow's exact loader data/dataloader_nba.py::NBADatasetMinMax
79
+ (the graph loader dataloader_nba_graph.py merely re-exports it). Same .npy
80
+ files (data/nba/original/nba_{train,test}.npy, 32500/12500 scenes,
81
+ 30 frames x 11 agents x 2D), same split (trajs[:n_train] / trajs[:n_test]),
82
+ same coordinate scaling traj_scale_total=94/28 and traj_mean=[14,7.5].
83
+ * Past = 10 frames, Future = 20 frames = 4.0 s @ 5 Hz, 11 agents, 2D.
84
+ * Encoder input = data['past_traj'] (6-dim min-max normalized to [-1,1]).
85
+ * Coordinate frame for the GMM/interaction = data['past_traj_original_scale']
86
+ (centered-abs = pos/scale - traj_mean); cur_xy = its last past step.
87
+ Ground-truth future displacement = data['fut_traj_original_scale']
88
+ (= future - last_observed, in /scale units) -- the SAME tensor the MoFlow
89
+ evaluator (eval_perscene_moflow.py) compares against.
90
+ * Metric (identical to eval_perscene_moflow.py, verified vs Table 1):
91
+ d = ||pred_disp - gt_disp|| # [B,A,K,T]
92
+ ADE4 = d[..., :20].mean(over T).min(over K), mean over 11 agents & scenes
93
+ FDE4 = d[..., 19] .min(over K), mean over 11 agents & scenes
94
+ i.e. marginal min-of-K=20 at the 4.0 s horizon (frame 20). Table-1 baselines
95
+ are ~0.70-1.04 ADE in this space.
96
+
97
+ Prints: [GF-NBA] epoch N ADE4=.. FDE4=..
98
+
99
+ DO NOT run on GPU here (GPUs busy) -- this file is only `python -m py_compile`d.
100
+ """
101
+
102
+ import os
103
+ import sys
104
+ import argparse
105
+ import time
106
+
107
+ # ------------------------------------------------------------------ GPU FIRST
108
+ # CUDA context is fixed at torch import; select the device BEFORE importing torch
109
+ def _early_gpu():
110
+ for i, a in enumerate(sys.argv):
111
+ if a == '--gpu' and i + 1 < len(sys.argv):
112
+ return sys.argv[i + 1]
113
+ if a.startswith('--gpu='):
114
+ return a.split('=', 1)[1]
115
+ return None
116
+
117
+
118
+ _g = _early_gpu()
119
+ if _g is not None:
120
+ os.environ['CUDA_VISIBLE_DEVICES'] = str(_g)
121
+ os.environ.setdefault('MPLBACKEND', 'Agg') # dataloader imports matplotlib
122
+
123
+ # ------------------------------------------------ reuse MoFlow's NBA pipeline
124
+ MOFLOW_ROOT = '/mnt/jaewoo4tb/srtp/MoFlow'
125
+ if MOFLOW_ROOT not in sys.path:
126
+ sys.path.insert(0, MOFLOW_ROOT)
127
+
128
+ import math
129
+ import types as _types
130
+ import numpy as np
131
+ import torch
132
+ import torch.nn as nn
133
+ import torch.nn.functional as F
134
+ from torch.utils.data import DataLoader
135
+
136
+ # so that `from utils.normalization import ...` inside the loader resolves
137
+ try:
138
+ os.chdir(MOFLOW_ROOT)
139
+ except Exception:
140
+ pass
141
+ from data.dataloader_nba import NBADatasetMinMax, seq_collate_nba
142
+
143
+
144
+ # ============================================================================
145
+ # MODEL (faithful GameFormer, NBA-adapted: no map)
146
+ # ============================================================================
147
+ D_MODEL = 256
148
+ N_HEADS = 8
149
+ DROPOUT = 0.1
150
+
151
+
152
+ # ---------------------------------------------------------------- primitives
153
+ class SelfTransformer(nn.Module):
154
+ """modules.py::SelfTransformer (verbatim)."""
155
+
156
+ def __init__(self, dim=D_MODEL, heads=N_HEADS, dropout=DROPOUT):
157
+ super().__init__()
158
+ self.self_attention = nn.MultiheadAttention(dim, heads, dropout, batch_first=True)
159
+ self.norm_1 = nn.LayerNorm(dim)
160
+ self.norm_2 = nn.LayerNorm(dim)
161
+ self.ffn = nn.Sequential(
162
+ nn.Linear(dim, dim * 4), nn.GELU(), nn.Dropout(dropout),
163
+ nn.Linear(dim * 4, dim), nn.Dropout(dropout))
164
+
165
+ def forward(self, inputs, mask=None):
166
+ attn, _ = self.self_attention(inputs, inputs, inputs, key_padding_mask=mask)
167
+ attn = self.norm_1(attn + inputs)
168
+ return self.norm_2(self.ffn(attn) + attn)
169
+
170
+
171
+ class CrossTransformer(nn.Module):
172
+ """modules.py::CrossTransformer (verbatim)."""
173
+
174
+ def __init__(self, dim=D_MODEL, heads=N_HEADS, dropout=DROPOUT):
175
+ super().__init__()
176
+ self.cross_attention = nn.MultiheadAttention(dim, heads, dropout, batch_first=True)
177
+ self.norm_1 = nn.LayerNorm(dim)
178
+ self.norm_2 = nn.LayerNorm(dim)
179
+ self.ffn = nn.Sequential(
180
+ nn.Linear(dim, dim * 4), nn.GELU(), nn.Dropout(dropout),
181
+ nn.Linear(dim * 4, dim), nn.Dropout(dropout))
182
+
183
+ def forward(self, query, key, value, mask=None):
184
+ attn, _ = self.cross_attention(query, key, value, key_padding_mask=mask)
185
+ attn = self.norm_1(attn)
186
+ return self.norm_2(self.ffn(attn) + attn)
187
+
188
+
189
+ class GMMPredictor(nn.Module):
190
+ """modules.py::GMMPredictor (verbatim). Out: (mu_x,mu_y,log_sig_x,log_sig_y)."""
191
+
192
+ def __init__(self, future_len, dim=D_MODEL):
193
+ super().__init__()
194
+ self._future_len = future_len
195
+ self.gaussian = nn.Sequential(
196
+ nn.Linear(dim, 512), nn.ELU(), nn.Dropout(0.1),
197
+ nn.Linear(512, future_len * 4))
198
+ self.score = nn.Sequential(
199
+ nn.Linear(dim, 64), nn.ELU(), nn.Dropout(0.1), nn.Linear(64, 1))
200
+
201
+ def forward(self, x):
202
+ # x: [B, M, D]
203
+ B, M, _ = x.shape
204
+ res = self.gaussian(x).view(B, M, self._future_len, 4)
205
+ score = self.score(x).squeeze(-1) # [B, M]
206
+ return res, score
207
+
208
+
209
+ # ------------------------------------------------------------------ encoder
210
+ class AgentHistoryEncoder(nn.Module):
211
+ """modules.py::AgentEncoder -- 2-layer LSTM(->256) over agent history + type
212
+ embedding. GameFormer uses nn.LSTM(8,256,2); NBA feature dim is 6."""
213
+
214
+ def __init__(self, in_dim=6, dim=D_MODEL, n_types=2):
215
+ super().__init__()
216
+ self.motion = nn.LSTM(in_dim, dim, 2, batch_first=True)
217
+ self.type_emb = nn.Embedding(n_types, dim)
218
+
219
+ def forward(self, hist, types):
220
+ # hist: [B, A, T, in_dim] ; types: [A] long
221
+ B, A, T, C = hist.shape
222
+ traj, _ = self.motion(hist.reshape(B * A, T, C))
223
+ out = traj[:, -1].reshape(B, A, -1) # [B, A, D]
224
+ out = out + self.type_emb(types)[None] # + per-agent type emb
225
+ return out
226
+
227
+
228
+ class FusionEncoder(nn.Module):
229
+ """GameFormer.Encoder.fusion_encoder -- self-attention over agent tokens.
230
+ (No map tokens for NBA.)"""
231
+
232
+ def __init__(self, dim=D_MODEL, heads=N_HEADS, layers=6, dropout=DROPOUT):
233
+ super().__init__()
234
+ layer = nn.TransformerEncoderLayer(
235
+ d_model=dim, nhead=heads, dim_feedforward=dim * 4,
236
+ activation=F.gelu, dropout=dropout, batch_first=True)
237
+ self.encoder = nn.TransformerEncoder(layer, layers, enable_nested_tensor=False)
238
+
239
+ def forward(self, tokens, mask=None):
240
+ return self.encoder(tokens, src_key_padding_mask=mask)
241
+
242
+
243
+ # ------------------------------------------------------------- future encoder
244
+ class FutureEncoder(nn.Module):
245
+ """modules.py::FutureEncoder -- encodes each agent's K modal futures into a
246
+ per-mode vector via max-pool over an MLP on [x,y,heading,vx,vy]. GameFormer
247
+ appends box size (w,l,h) -> dropped for NBA (8 -> 5 input dims)."""
248
+
249
+ def __init__(self, dim=D_MODEL, n_types=2, dt=0.2):
250
+ super().__init__()
251
+ self.dt = dt
252
+ self.mlp = nn.Sequential(nn.Linear(5, 64), nn.ReLU(), nn.Linear(64, dim))
253
+ self.type_emb = nn.Embedding(n_types, dim)
254
+
255
+ def state_process(self, trajs, cur_xy):
256
+ # trajs: [B, A, M, T, 2] ; cur_xy: [B, A, 2]
257
+ B, A, M, T, _ = trajs.shape
258
+ cur = cur_xy[:, :, None, None, :].expand(B, A, M, 1, 2)
259
+ xy = torch.cat([cur, trajs], dim=-2) # [B,A,M,T+1,2]
260
+ dxy = torch.diff(xy, dim=-2) # [B,A,M,T,2]
261
+ v = dxy / self.dt
262
+ theta = torch.atan2(dxy[..., 1], dxy[..., 0].clamp(min=1e-3)).unsqueeze(-1)
263
+ return torch.cat([trajs, theta, v], dim=-1) # [B,A,M,T,5]
264
+
265
+ def forward(self, trajs, cur_xy, types):
266
+ feat = self.state_process(trajs, cur_xy)
267
+ h = self.mlp(feat.detach()) # GameFormer detaches
268
+ out = torch.max(h, dim=-2).values # [B,A,M,D]
269
+ out = out + self.type_emb(types)[None, :, None, :] # + type emb
270
+ return out
271
+
272
+
273
+ # ----------------------------------------------------------- level-0 decoder
274
+ class InitialDecoder(nn.Module):
275
+ """modules.py::InitialDecoder -- vectorized over agents."""
276
+
277
+ def __init__(self, modalities, n_agents, future_len, dim=D_MODEL):
278
+ super().__init__()
279
+ self.M = modalities
280
+ self.A = n_agents
281
+ self.multi_modal_query_embedding = nn.Embedding(modalities, dim)
282
+ self.agent_query_embedding = nn.Embedding(n_agents, dim)
283
+ self.query_encoder = CrossTransformer(dim)
284
+ self.predictor = GMMPredictor(future_len, dim)
285
+ self.register_buffer('modal', torch.arange(modalities).long())
286
+ self.register_buffer('agent', torch.arange(n_agents).long())
287
+
288
+ def forward(self, encoding, cur_xy, mask=None):
289
+ # encoding: [B, A, D] (full agent context, shared as keys)
290
+ B, A, D = encoding.shape
291
+ M = self.M
292
+ multi_modal = self.multi_modal_query_embedding(self.modal) # [M, D]
293
+ agent = self.agent_query_embedding(self.agent) # [A, D]
294
+ mm_agent_query = multi_modal[None, :, :] + agent[:, None, :] # [A, M, D]
295
+ query = encoding[:, :, None, :] + mm_agent_query[None] # [B, A, M, D]
296
+
297
+ q = query.reshape(B * A, M, D)
298
+ kv = encoding[:, None, :, :].expand(B, A, A, D).reshape(B * A, A, D)
299
+ km = None
300
+ if mask is not None:
301
+ km = mask[:, None, :].expand(B, A, A).reshape(B * A, A)
302
+ content = self.query_encoder(q, kv, kv, km) # [B*A, M, D]
303
+ preds, scores = self.predictor(content) # [B*A,M,T,4],[B*A,M]
304
+
305
+ content = content.reshape(B, A, M, D)
306
+ T = preds.shape[-2]
307
+ preds = preds.reshape(B, A, M, T, 4)
308
+ scores = scores.reshape(B, A, M)
309
+ preds = preds.clone()
310
+ preds[..., :2] = preds[..., :2] + cur_xy[:, :, None, None, :]
311
+ return content, preds, scores
312
+
313
+
314
+ # ------------------------------------------------------- level-k decoder
315
+ class InteractionDecoder(nn.Module):
316
+ """modules.py::InteractionDecoder -- one game-theoretic reasoning level,
317
+ vectorized over agents. Conditions each agent on ALL agents' level-(k-1)
318
+ predictions (interaction self-attention), masking the agent's own future."""
319
+
320
+ def __init__(self, future_encoder, future_len, n_agents, dim=D_MODEL):
321
+ super().__init__()
322
+ self.A = n_agents
323
+ self.future_encoder = future_encoder # SHARED across levels
324
+ self.interaction_encoder = SelfTransformer(dim)
325
+ self.query_encoder = CrossTransformer(dim)
326
+ self.decoder = GMMPredictor(future_len, dim)
327
+
328
+ def forward(self, cur_xy, last_traj, last_scores, last_content, encoding, types, mask=None):
329
+ # cur_xy:[B,A,2] last_traj:[B,A,M,T,4] last_scores:[B,A,M]
330
+ # last_content:[B,A,M,D] encoding:[B,A,D]
331
+ B, A, M, T, _ = last_traj.shape
332
+ D = encoding.shape[-1]
333
+
334
+ # encode all agents' level-(k-1) modal futures
335
+ multi_futures = self.future_encoder(last_traj[..., :2], cur_xy, types) # [B,A,M,D]
336
+ futures = (multi_futures * last_scores.softmax(-1).unsqueeze(-1)).mean(dim=2) # [B,A,D]
337
+
338
+ # interaction over agents (self-attention)
339
+ interaction = self.interaction_encoder(futures, None) # [B,A,D]
340
+
341
+ # combined context = [interaction tokens ; context tokens]
342
+ ctx = torch.cat([interaction, encoding], dim=1) # [B, 2A, D]
343
+ # per-agent key mask: mask each agent's OWN future token in the 1st block
344
+ km = torch.zeros(B, A, 2 * A, dtype=torch.bool, device=encoding.device)
345
+ idx = torch.arange(A, device=encoding.device)
346
+ km[:, idx, idx] = True
347
+ if mask is not None:
348
+ # second block inherits context validity mask (all-valid for NBA)
349
+ km[:, :, A:] = km[:, :, A:] | mask[:, None, :]
350
+ km[:, :, :A] = km[:, :, :A] | mask[:, None, :]
351
+ km = km.reshape(B * A, 2 * A)
352
+ kv = ctx[:, None, :, :].expand(B, A, 2 * A, D).reshape(B * A, 2 * A, D)
353
+
354
+ query = (last_content + multi_futures).reshape(B * A, M, D) # prev content + own future
355
+ content = self.query_encoder(query, kv, kv, km) # [B*A, M, D]
356
+ traj, scores = self.decoder(content) # [B*A,M,T,4],[B*A,M]
357
+
358
+ content = content.reshape(B, A, M, D)
359
+ traj = traj.reshape(B, A, M, T, 4)
360
+ scores = scores.reshape(B, A, M)
361
+ traj = traj.clone()
362
+ traj[..., :2] = traj[..., :2] + cur_xy[:, :, None, None, :]
363
+ return content, traj, scores
364
+
365
+
366
+ # -------------------------------------------------------------- full decoder
367
+ class GameFormerDecoder(nn.Module):
368
+ """GameFormer.Decoder -- level 0 + L interaction levels (shared FutureEncoder)."""
369
+
370
+ def __init__(self, modalities, n_agents, future_len, levels, dim=D_MODEL,
371
+ n_types=2, dt=0.2):
372
+ super().__init__()
373
+ self._levels = levels
374
+ self.future_encoder = FutureEncoder(dim, n_types, dt)
375
+ self.initial_stage = InitialDecoder(modalities, n_agents, future_len, dim)
376
+ self.interaction_stage = nn.ModuleList(
377
+ [InteractionDecoder(self.future_encoder, future_len, n_agents, dim)
378
+ for _ in range(levels)])
379
+
380
+ def forward(self, encoding, cur_xy, types, mask=None):
381
+ out = {}
382
+ content, traj, scores = self.initial_stage(encoding, cur_xy, mask)
383
+ out['level_0_interactions'] = traj
384
+ out['level_0_scores'] = scores
385
+ for k in range(1, self._levels + 1):
386
+ content, traj, scores = self.interaction_stage[k - 1](
387
+ cur_xy, traj, scores, content, encoding, types, mask)
388
+ out[f'level_{k}_interactions'] = traj
389
+ out[f'level_{k}_scores'] = scores
390
+ return out
391
+
392
+
393
+ # ---------------------------------------------------------------- top model
394
+ class GameFormerNBA(nn.Module):
395
+ """GameFormer.GameFormer (NBA, map dropped)."""
396
+
397
+ def __init__(self, n_agents=11, past_dim=6, future_len=20, modalities=20,
398
+ levels=2, dim=D_MODEL, heads=N_HEADS, enc_layers=6,
399
+ n_types=2, dt=0.2, ball_idx=10):
400
+ super().__init__()
401
+ self.levels = levels
402
+ self.history_encoder = AgentHistoryEncoder(past_dim, dim, n_types)
403
+ self.fusion_encoder = FusionEncoder(dim, heads, enc_layers)
404
+ self.decoder = GameFormerDecoder(modalities, n_agents, future_len, levels,
405
+ dim, n_types, dt)
406
+ types = torch.zeros(n_agents, dtype=torch.long)
407
+ if 0 <= ball_idx < n_agents and n_types > 1:
408
+ types[ball_idx] = 1
409
+ self.register_buffer('agent_types', types)
410
+
411
+ def forward(self, feats, cur_xy):
412
+ # feats: [B,A,T,past_dim] (encoder input) ; cur_xy: [B,A,2] (metric frame)
413
+ enc = self.history_encoder(feats, self.agent_types) # [B,A,D]
414
+ enc = self.fusion_encoder(enc, None) # [B,A,D]
415
+ return self.decoder(enc, cur_xy, self.agent_types, None)
416
+
417
+
418
+ # ============================================================================
419
+ # LOSS (level_k_loss + gmm_loss, gmm=True; MARGINAL per agent for NBA)
420
+ # ============================================================================
421
+ def gmm_loss_marginal(traj, conv, scores, gt, metric_idx):
422
+ """Per-agent GMM winner-takes-all (marginal version of gmm_loss).
423
+ traj: [B,A,M,T,2] conv: [B,A,M,T,2] scores: [B,A,M] gt: [B,A,T,2]
424
+ """
425
+ B, A, M, T, _ = traj.shape
426
+ dist = torch.norm(traj - gt[:, :, None], dim=-1) # [B,A,M,T]
427
+ ndist = dist.mean(-1) + dist[..., metric_idx].sum(-1) # [B,A,M]
428
+ best = ndist.argmin(-1) # [B,A]
429
+
430
+ gi = best[..., None, None, None].expand(B, A, 1, T, 2)
431
+ best_traj = torch.gather(traj, 2, gi).squeeze(2) # [B,A,T,2]
432
+ best_conv = torch.gather(conv, 2, gi).squeeze(2) # [B,A,T,2]
433
+
434
+ dx = best_traj[..., 0] - gt[..., 0]
435
+ dy = best_traj[..., 1] - gt[..., 1]
436
+ log_std_x = torch.clip(best_conv[..., 0], 0, 5)
437
+ log_std_y = torch.clip(best_conv[..., 1], 0, 5)
438
+ std_x, std_y = torch.exp(log_std_x), torch.exp(log_std_y)
439
+
440
+ reg = (log_std_x + log_std_y) + 0.5 * ((dx ** 2) / (std_x ** 2) + (dy ** 2) / (std_y ** 2))
441
+ reg = reg.mean(-1) + reg[..., metric_idx].sum(-1) # [B,A]
442
+
443
+ prob_loss = F.cross_entropy(scores.reshape(B * A, M), best.reshape(B * A),
444
+ label_smoothing=0.2)
445
+ return reg.mean() + 2.0 * prob_loss, best
446
+
447
+
448
+ def level_k_loss_marginal(outputs, gt_center, levels, metric_idx):
449
+ """Sum GMM loss across all levels 0..L (GameFormer's level-wise imitation)."""
450
+ total = 0.0
451
+ for k in range(levels + 1):
452
+ pred = outputs[f'level_{k}_interactions'] # [B,A,M,T,4]
453
+ scores = outputs[f'level_{k}_scores'] # [B,A,M]
454
+ traj, conv = pred[..., :2], pred[..., 2:]
455
+ l, _ = gmm_loss_marginal(traj, conv, scores, gt_center, metric_idx)
456
+ total = total + l
457
+ return total
458
+
459
+
460
+ # ============================================================================
461
+ # DATA
462
+ # ============================================================================
463
+ def build_loaders(args):
464
+ cfg = _types.SimpleNamespace(
465
+ traj_mean=[14, 7.5], data_norm='min_max',
466
+ past_frames=args.past_len, future_frames=args.future_len, agents=args.agents)
467
+ # Train first so cfg min/max scalars are populated for the test set.
468
+ train_set = NBADatasetMinMax(
469
+ obs_len=args.past_len, pred_len=args.future_len, training=True,
470
+ num_scenes=args.n_train, cfg=cfg, data_dir=args.data_dir,
471
+ rotate=False, data_norm='min_max')
472
+ test_set = NBADatasetMinMax(
473
+ obs_len=args.past_len, pred_len=args.future_len, training=False,
474
+ test_scenes=args.n_test, cfg=cfg, data_dir=args.data_dir,
475
+ rotate=False, data_norm='min_max')
476
+ train_loader = DataLoader(
477
+ train_set, batch_size=args.batch_size, shuffle=True, num_workers=args.workers,
478
+ collate_fn=seq_collate_nba, pin_memory=True, drop_last=True)
479
+ test_loader = DataLoader(
480
+ test_set, batch_size=args.test_batch, shuffle=False, num_workers=args.workers,
481
+ collate_fn=seq_collate_nba, pin_memory=True)
482
+ return train_loader, test_loader
483
+
484
+
485
+ def unpack(data, device):
486
+ feats = data['past_traj'].to(device) # [B,A,T,6] normalized
487
+ past_orig = data['past_traj_original_scale'].to(device) # [B,A,T,6] metric
488
+ gt_disp = data['fut_traj_original_scale'].to(device) # [B,A,Tf,2] displacement
489
+ cur_xy = past_orig[:, :, -1, 0:2] # centered-abs last pos
490
+ gt_center = gt_disp + cur_xy[:, :, None, :] # centered-abs future
491
+ return feats, cur_xy, gt_disp, gt_center
492
+
493
+
494
+ # ============================================================================
495
+ # EVAL (marginal min-of-K=20 at 4.0 s; identical to eval_perscene_moflow.py)
496
+ # ============================================================================
497
+ @torch.no_grad()
498
+ def evaluate(model, loader, device, levels, end):
499
+ model.eval()
500
+ ade_sum = fde_sum = 0.0
501
+ n = 0
502
+ for data in loader:
503
+ feats, cur_xy, gt_disp, _ = unpack(data, device)
504
+ out = model(feats, cur_xy)
505
+ pred = out[f'level_{levels}_interactions'][..., :2] # [B,A,M,T,2]
506
+ pred_disp = pred - cur_xy[:, :, None, None, :] # -> displacement
507
+ d = torch.norm(pred_disp - gt_disp[:, :, None], dim=-1) # [B,A,M,T]
508
+ ade = d[..., :end].mean(-1).min(dim=-1).values # [B,A] best-of-K
509
+ fde = d[..., end - 1].min(dim=-1).values # [B,A]
510
+ ade_sum += ade.sum().item()
511
+ fde_sum += fde.sum().item()
512
+ n += ade.numel()
513
+ return ade_sum / max(n, 1), fde_sum / max(n, 1)
514
+
515
+
516
+ # ============================================================================
517
+ # TRAIN
518
+ # ============================================================================
519
+ def parse_args():
520
+ p = argparse.ArgumentParser('GameFormer standalone predictor for NBA')
521
+ p.add_argument('--data_dir', default='/mnt/jaewoo4tb/srtp/MoFlow/data/nba', type=str)
522
+ p.add_argument('--gpu', default='0', type=str)
523
+ p.add_argument('--exp', default='gf_nba', type=str)
524
+ p.add_argument('--epochs', default=50, type=int)
525
+ p.add_argument('--batch_size', default=128, type=int)
526
+ p.add_argument('--test_batch', default=500, type=int)
527
+ p.add_argument('--workers', default=4, type=int)
528
+ p.add_argument('--seed', default=3407, type=int) # GameFormer default
529
+
530
+ # --- architecture (faithful GameFormer knobs) ---
531
+ p.add_argument('--modalities', default=20, type=int, help='K modes (NBA min-ADE_20)')
532
+ p.add_argument('--level', default=2, type=int, help='game-theoretic levels (paper: 3)')
533
+ p.add_argument('--encoder_layers', default=6, type=int)
534
+ p.add_argument('--agents', default=11, type=int)
535
+ p.add_argument('--past_len', default=10, type=int)
536
+ p.add_argument('--future_len', default=20, type=int) # 4.0 s @ 5 Hz
537
+ p.add_argument('--ball_idx', default=10, type=int, help='-1 to disable type emb')
538
+
539
+ # --- optimization (GameFormer: AdamW 1e-4, MultiStepLR x0.5, clip 5) ---
540
+ p.add_argument('--lr', default=1e-4, type=float)
541
+ p.add_argument('--weight_decay', default=1e-4, type=float)
542
+ p.add_argument('--grad_clip', default=5.0, type=float)
543
+ p.add_argument('--n_train', default=32500, type=int)
544
+ p.add_argument('--n_test', default=12500, type=int)
545
+ p.add_argument('--eval_every', default=2, type=int)
546
+ p.add_argument('--save_dir', default=os.path.join(
547
+ os.path.dirname(os.path.abspath(__file__)), 'gf_nba_ckpt'), type=str)
548
+ return p.parse_args()
549
+
550
+
551
+ def main():
552
+ args = parse_args()
553
+ torch.manual_seed(args.seed)
554
+ np.random.seed(args.seed)
555
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
556
+
557
+ train_loader, test_loader = build_loaders(args)
558
+
559
+ n_types = 2 if (0 <= args.ball_idx < args.agents) else 1
560
+ model = GameFormerNBA(
561
+ n_agents=args.agents, past_dim=6, future_len=args.future_len,
562
+ modalities=args.modalities, levels=args.level, dim=D_MODEL, heads=N_HEADS,
563
+ enc_layers=args.encoder_layers, n_types=n_types, dt=0.2,
564
+ ball_idx=args.ball_idx).to(device)
565
+ n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
566
+ print(f'[GF-NBA] model params: {n_params/1e6:.2f} M | levels={args.level} '
567
+ f'K={args.modalities} enc_layers={args.encoder_layers} device={device}')
568
+
569
+ opt = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
570
+ milestones = sorted(set(int(args.epochs * f) for f in (0.5, 0.65, 0.8, 0.9)))
571
+ milestones = [m for m in milestones if 0 < m < args.epochs]
572
+ sched = torch.optim.lr_scheduler.MultiStepLR(opt, milestones=milestones, gamma=0.5)
573
+
574
+ end = args.future_len # 4.0 s = frame 20
575
+ metric_idx = sorted(set([max(0, args.future_len // 2 - 1), args.future_len - 1]))
576
+
577
+ os.makedirs(args.save_dir, exist_ok=True)
578
+ best_ade = float('inf')
579
+
580
+ for epoch in range(args.epochs):
581
+ model.train()
582
+ t0 = time.time()
583
+ running = 0.0
584
+ nb = 0
585
+ for data in train_loader:
586
+ feats, cur_xy, _, gt_center = unpack(data, device)
587
+ out = model(feats, cur_xy)
588
+ loss = level_k_loss_marginal(out, gt_center, args.level, metric_idx)
589
+ opt.zero_grad()
590
+ loss.backward()
591
+ torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
592
+ opt.step()
593
+ running += float(loss.item())
594
+ nb += 1
595
+ sched.step()
596
+ print(f'[GF-NBA] epoch {epoch+1}/{args.epochs} loss={running/max(nb,1):.4f} '
597
+ f'lr={opt.param_groups[0]["lr"]:.2e} ({time.time()-t0:.1f}s)', flush=True)
598
+
599
+ if (epoch + 1) % args.eval_every == 0 or epoch == args.epochs - 1:
600
+ ade, fde = evaluate(model, test_loader, device, args.level, end)
601
+ print(f'[GF-NBA] epoch {epoch+1} ADE4={ade:.4f} FDE4={fde:.4f}', flush=True)
602
+ if ade < best_ade:
603
+ best_ade = ade
604
+ torch.save({'model': model.state_dict(), 'epoch': epoch + 1,
605
+ 'ade4': ade, 'fde4': fde, 'args': vars(args)},
606
+ os.path.join(args.save_dir, f'{args.exp}_best.pt'))
607
+ print(f'[GF-NBA] saved best (ADE4={ade:.4f}) -> '
608
+ f'{os.path.join(args.save_dir, args.exp + "_best.pt")}', flush=True)
609
+
610
+ print(f'[GF-NBA] done. best ADE4={best_ade:.4f}', flush=True)
611
+
612
+
613
+ if __name__ == '__main__':
614
+ main()
standalone/gameformer_sra_nba_standalone.py ADDED
@@ -0,0 +1,712 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ gameformer_sra_nba_standalone.py
5
+ ================================================================================
6
+ SRA-AUGMENTED standalone GameFormer for NBA. Base = the faithful standalone
7
+ GameFormer (Liu et al., ICCV 2023); this file INJECTS SRA's three philosophies
8
+ into GameFormer's level-k InteractionDecoder to test whether the SRA idea
9
+ generalizes to a NON-diffusion (feedforward) predictor.
10
+
11
+ SRA components injected (env-gated; GF_SRA=1 enables all):
12
+ 1. NEIGHBOR SELECTION (GF_SRA_TOPN) -- sparse RAG top-N (=GF_TOPN, default 5)
13
+ neighbor selection from future proximity; replaces GameFormer's DENSE
14
+ agent<->agent interaction with a sparse one (self-attn attn_mask + cross-
15
+ attn key mask restricted to each agent's top-N neighbors).
16
+ 2. UNCERTAINTY GATING (GF_SRA_SIGMA) -- turns the per-step GMM log-sigma
17
+ (which GameFormer emits but never USES for interaction) into a learned
18
+ message gate in (0,1): uncertain agents send weaker interaction messages
19
+ (uncertainty-weighted message passing). Also biases neighbor selection
20
+ toward confident agents (GF_SIG_SEL_W).
21
+ 3. RELATIONAL FUTURE FEATURES (GF_SRA_RELFUT) -- augments GameFormer's
22
+ per-agent future encoding with explicit PAIRWISE-RELATIVE future features
23
+ (MLP over relative position+velocity, max-pooled over time, neighbor-mean).
24
+
25
+ ALL FLAGS OFF == vanilla GameFormer (identical to gameformer_nba_standalone,
26
+ ADE4/FDE4 = 0.848/0.951). So (vanilla) vs (GF_SRA=1) is a clean isolated
27
+ ablation of "does SRA's philosophy help a feedforward predictor too?".
28
+
29
+ It is a STANDALONE predictor: it consumes ONLY agent HISTORY (past trajectories)
30
+ and predicts K multi-modal futures via the level-k game-theoretic decoder. It
31
+ does NOT consume any external denoiser / diffusion prediction.
32
+
33
+ --------------------------------------------------------------------------------
34
+ WHAT WAS REPLICATED (evidence = MCZhi/GameFormer @ main)
35
+ --------------------------------------------------------------------------------
36
+ Reference files studied verbatim:
37
+ * model/GameFormer.py (Encoder, Decoder, GameFormer)
38
+ * model/modules.py (AgentEncoder, FutureEncoder, GMMPredictor,
39
+ SelfTransformer, CrossTransformer,
40
+ InitialDecoder, InteractionDecoder)
41
+ * utils/inter_pred_utils.py (level_k_loss, gmm_loss)
42
+ * interaction_prediction/train.py (AdamW 1e-4, MultiStepLR, grad-clip 5,
43
+ level=3, modalities=6, encoder_layers=6)
44
+
45
+ ENCODER (GameFormer.Encoder + modules.AgentEncoder)
46
+ * Per-agent temporal encoder = 2-layer LSTM, hidden 256 (nn.LSTM(8,256,2)).
47
+ We adapt the input feature dim 8 -> 6 (NBA has [abs xy, rel xy, vel xy], no
48
+ heading/size channels). A learned agent-type embedding (player vs. ball) is
49
+ added to the LSTM last-step output, exactly as GameFormer adds `type_emb`.
50
+ * Interaction / self-attention over agents = the GameFormer `fusion_encoder`,
51
+ a nn.TransformerEncoder of TransformerEncoderLayer(d_model=256, nhead=8,
52
+ dim_feedforward=1024, activation=gelu, dropout=0.1, batch_first=True),
53
+ `encoder_layers` deep. In GameFormer this fuses agent tokens WITH per-agent
54
+ map (lane/crosswalk) tokens; NBA has NO map, so the map encoders
55
+ (LaneEncoder / CrosswalkEncoder / segment_map) are DROPPED and the fusion is
56
+ pure agent<->agent self-attention over the 11 tokens (identical for every
57
+ target agent when there is no per-agent map, so it is computed once).
58
+
59
+ LEVEL-k GAME-THEORETIC DECODER (GameFormer.Decoder)
60
+ * Level 0 = InitialDecoder: for each agent, a set of `modalities` (=K) learned
61
+ modal query embeddings + a per-agent query embedding are added to the agent's
62
+ own context token, then cross-attended (CrossTransformer) to the full agent
63
+ context. A GMMPredictor head outputs K trajectories of (mu_x,mu_y,
64
+ log_sig_x,log_sig_y) per step + K mode scores. predictions[...,:2] += the
65
+ agent's current (last observed) position.
66
+ * Levels 1..L = InteractionDecoder (one per level, sharing ONE FutureEncoder):
67
+ - FutureEncoder encodes EVERY agent's K level-(k-1) modal futures into a
68
+ vector (max-pool over time of an MLP on [x,y,heading,vx,vy]); GameFormer
69
+ also appends box size (w,l,h) -> we drop those (no size in NBA), 8->5.
70
+ - The per-mode future encodings are score-softmax-weighted and mean-pooled
71
+ to one vector per agent, then passed through a SelfTransformer to produce
72
+ the interaction encoding over all agents (this is the game-theoretic
73
+ "condition on all other agents' level-(k-1) predictions" step).
74
+ - The interaction tokens are concatenated with the context tokens; each
75
+ agent's OWN future token is masked out; the previous-level content plus
76
+ the agent's own future encoding form the query, cross-attended to that
77
+ combined context, and a fresh GMMPredictor emits the level-k K futures.
78
+ * Output dict: level_0_interactions/scores ... level_L_interactions/scores,
79
+ exactly as GameFormer.Decoder.forward.
80
+
81
+ LOSS (utils.inter_pred_utils.level_k_loss + gmm_loss, gmm=True)
82
+ * Summed over ALL levels k=0..L (level-wise imitation, GameFormer's key term).
83
+ * Per level, GMM winner-takes-all: best mode = argmin over K of
84
+ (mean_t ||traj-gt|| + sum over checkpoint steps), then the Gaussian NLL
85
+ reg = (log_sig_x+log_sig_y) + 0.5*(dx^2/sx^2 + dy^2/sy^2) with log_sig
86
+ clipped to [0,5], reduced as mean_t + sum over checkpoints; plus mode
87
+ classification F.cross_entropy(scores, best_mode, label_smoothing=0.2)
88
+ weighted x2. We MARGINALIZE per agent (GameFormer's interaction_prediction
89
+ task predicts 2 agents JOINTLY via distance.mean(1)/scores.sum(1); the NBA
90
+ Table-1 metric is MARGINAL min-ADE_20, so the best mode / scores are taken
91
+ per agent instead of jointly -- the only loss adaptation).
92
+
93
+ --------------------------------------------------------------------------------
94
+ NBA I/O + METRIC (matched to MoFlow / Table 1)
95
+ --------------------------------------------------------------------------------
96
+ * Data: reuses MoFlow's exact loader data/dataloader_nba.py::NBADatasetMinMax
97
+ (the graph loader dataloader_nba_graph.py merely re-exports it). Same .npy
98
+ files (data/nba/original/nba_{train,test}.npy, 32500/12500 scenes,
99
+ 30 frames x 11 agents x 2D), same split (trajs[:n_train] / trajs[:n_test]),
100
+ same coordinate scaling traj_scale_total=94/28 and traj_mean=[14,7.5].
101
+ * Past = 10 frames, Future = 20 frames = 4.0 s @ 5 Hz, 11 agents, 2D.
102
+ * Encoder input = data['past_traj'] (6-dim min-max normalized to [-1,1]).
103
+ * Coordinate frame for the GMM/interaction = data['past_traj_original_scale']
104
+ (centered-abs = pos/scale - traj_mean); cur_xy = its last past step.
105
+ Ground-truth future displacement = data['fut_traj_original_scale']
106
+ (= future - last_observed, in /scale units) -- the SAME tensor the MoFlow
107
+ evaluator (eval_perscene_moflow.py) compares against.
108
+ * Metric (identical to eval_perscene_moflow.py, verified vs Table 1):
109
+ d = ||pred_disp - gt_disp|| # [B,A,K,T]
110
+ ADE4 = d[..., :20].mean(over T).min(over K), mean over 11 agents & scenes
111
+ FDE4 = d[..., 19] .min(over K), mean over 11 agents & scenes
112
+ i.e. marginal min-of-K=20 at the 4.0 s horizon (frame 20). Table-1 baselines
113
+ are ~0.70-1.04 ADE in this space.
114
+
115
+ Prints: [SRA-GF] epoch N ADE4=.. FDE4=..
116
+
117
+ DO NOT run on GPU here (GPUs busy) -- this file is only `python -m py_compile`d.
118
+ """
119
+
120
+ import os
121
+ import sys
122
+ import argparse
123
+ import time
124
+
125
+ # ------------------------------------------------------------------ GPU FIRST
126
+ # CUDA context is fixed at torch import; select the device BEFORE importing torch
127
+ def _early_gpu():
128
+ for i, a in enumerate(sys.argv):
129
+ if a == '--gpu' and i + 1 < len(sys.argv):
130
+ return sys.argv[i + 1]
131
+ if a.startswith('--gpu='):
132
+ return a.split('=', 1)[1]
133
+ return None
134
+
135
+
136
+ _g = _early_gpu()
137
+ if _g is not None:
138
+ os.environ['CUDA_VISIBLE_DEVICES'] = str(_g)
139
+ os.environ.setdefault('MPLBACKEND', 'Agg') # dataloader imports matplotlib
140
+
141
+ # ------------------------------------------------ reuse MoFlow's NBA pipeline
142
+ MOFLOW_ROOT = '/mnt/jaewoo4tb/srtp/MoFlow'
143
+ if MOFLOW_ROOT not in sys.path:
144
+ sys.path.insert(0, MOFLOW_ROOT)
145
+
146
+ import math
147
+ import types as _types
148
+ import numpy as np
149
+ import torch
150
+ import torch.nn as nn
151
+ import torch.nn.functional as F
152
+ from torch.utils.data import DataLoader
153
+
154
+ # so that `from utils.normalization import ...` inside the loader resolves
155
+ try:
156
+ os.chdir(MOFLOW_ROOT)
157
+ except Exception:
158
+ pass
159
+ from data.dataloader_nba import NBADatasetMinMax, seq_collate_nba
160
+
161
+
162
+ # ============================================================================
163
+ # MODEL (faithful GameFormer, NBA-adapted: no map)
164
+ # ============================================================================
165
+ D_MODEL = 256
166
+ N_HEADS = 8
167
+ DROPOUT = 0.1
168
+
169
+
170
+ # ---------------------------------------------------------------- SRA toggles
171
+ # Inject SRA's three philosophies into GameFormer's level-k InteractionDecoder.
172
+ # Master flag GF_SRA=1 enables all; or enable individually. All-off == vanilla
173
+ # GameFormer (identical to gameformer_nba_standalone.py, 0.848/0.951).
174
+ def _sra_on(name):
175
+ if os.environ.get('GF_SRA', '') not in ('', '0', 'false', 'False'):
176
+ return True
177
+ return os.environ.get(name, '') not in ('', '0', 'false', 'False')
178
+
179
+
180
+ USE_TOPN = _sra_on('GF_SRA_TOPN') # sparse RAG top-N neighbor selection
181
+ USE_SIGMA = _sra_on('GF_SRA_SIGMA') # uncertainty-gated message passing
182
+ USE_RELFUT = _sra_on('GF_SRA_RELFUT') # explicit relational future features
183
+ TOPN_K = int(os.environ.get('GF_TOPN', '5')) # SRA neighbor budget (top_n=5)
184
+ SIG_SEL_W = float(os.environ.get('GF_SIG_SEL_W', '0.5')) # sigma weight in nbr selection
185
+
186
+
187
+ # ---------------------------------------------------------------- primitives
188
+ class SelfTransformer(nn.Module):
189
+ """modules.py::SelfTransformer. `attn_mask` added for SRA sparse-neighbor
190
+ selection (per-query [B*heads,A,A] boolean; True = block non-neighbor)."""
191
+
192
+ def __init__(self, dim=D_MODEL, heads=N_HEADS, dropout=DROPOUT):
193
+ super().__init__()
194
+ self.heads = heads
195
+ self.self_attention = nn.MultiheadAttention(dim, heads, dropout, batch_first=True)
196
+ self.norm_1 = nn.LayerNorm(dim)
197
+ self.norm_2 = nn.LayerNorm(dim)
198
+ self.ffn = nn.Sequential(
199
+ nn.Linear(dim, dim * 4), nn.GELU(), nn.Dropout(dropout),
200
+ nn.Linear(dim * 4, dim), nn.Dropout(dropout))
201
+
202
+ def forward(self, inputs, mask=None, attn_mask=None):
203
+ attn, _ = self.self_attention(inputs, inputs, inputs, key_padding_mask=mask,
204
+ attn_mask=attn_mask)
205
+ attn = self.norm_1(attn + inputs)
206
+ return self.norm_2(self.ffn(attn) + attn)
207
+
208
+
209
+ class CrossTransformer(nn.Module):
210
+ """modules.py::CrossTransformer (verbatim)."""
211
+
212
+ def __init__(self, dim=D_MODEL, heads=N_HEADS, dropout=DROPOUT):
213
+ super().__init__()
214
+ self.cross_attention = nn.MultiheadAttention(dim, heads, dropout, batch_first=True)
215
+ self.norm_1 = nn.LayerNorm(dim)
216
+ self.norm_2 = nn.LayerNorm(dim)
217
+ self.ffn = nn.Sequential(
218
+ nn.Linear(dim, dim * 4), nn.GELU(), nn.Dropout(dropout),
219
+ nn.Linear(dim * 4, dim), nn.Dropout(dropout))
220
+
221
+ def forward(self, query, key, value, mask=None):
222
+ attn, _ = self.cross_attention(query, key, value, key_padding_mask=mask)
223
+ attn = self.norm_1(attn)
224
+ return self.norm_2(self.ffn(attn) + attn)
225
+
226
+
227
+ class GMMPredictor(nn.Module):
228
+ """modules.py::GMMPredictor (verbatim). Out: (mu_x,mu_y,log_sig_x,log_sig_y)."""
229
+
230
+ def __init__(self, future_len, dim=D_MODEL):
231
+ super().__init__()
232
+ self._future_len = future_len
233
+ self.gaussian = nn.Sequential(
234
+ nn.Linear(dim, 512), nn.ELU(), nn.Dropout(0.1),
235
+ nn.Linear(512, future_len * 4))
236
+ self.score = nn.Sequential(
237
+ nn.Linear(dim, 64), nn.ELU(), nn.Dropout(0.1), nn.Linear(64, 1))
238
+
239
+ def forward(self, x):
240
+ # x: [B, M, D]
241
+ B, M, _ = x.shape
242
+ res = self.gaussian(x).view(B, M, self._future_len, 4)
243
+ score = self.score(x).squeeze(-1) # [B, M]
244
+ return res, score
245
+
246
+
247
+ # ------------------------------------------------------------------ encoder
248
+ class AgentHistoryEncoder(nn.Module):
249
+ """modules.py::AgentEncoder -- 2-layer LSTM(->256) over agent history + type
250
+ embedding. GameFormer uses nn.LSTM(8,256,2); NBA feature dim is 6."""
251
+
252
+ def __init__(self, in_dim=6, dim=D_MODEL, n_types=2):
253
+ super().__init__()
254
+ self.motion = nn.LSTM(in_dim, dim, 2, batch_first=True)
255
+ self.type_emb = nn.Embedding(n_types, dim)
256
+
257
+ def forward(self, hist, types):
258
+ # hist: [B, A, T, in_dim] ; types: [A] long
259
+ B, A, T, C = hist.shape
260
+ traj, _ = self.motion(hist.reshape(B * A, T, C))
261
+ out = traj[:, -1].reshape(B, A, -1) # [B, A, D]
262
+ out = out + self.type_emb(types)[None] # + per-agent type emb
263
+ return out
264
+
265
+
266
+ class FusionEncoder(nn.Module):
267
+ """GameFormer.Encoder.fusion_encoder -- self-attention over agent tokens.
268
+ (No map tokens for NBA.)"""
269
+
270
+ def __init__(self, dim=D_MODEL, heads=N_HEADS, layers=6, dropout=DROPOUT):
271
+ super().__init__()
272
+ layer = nn.TransformerEncoderLayer(
273
+ d_model=dim, nhead=heads, dim_feedforward=dim * 4,
274
+ activation=F.gelu, dropout=dropout, batch_first=True)
275
+ self.encoder = nn.TransformerEncoder(layer, layers, enable_nested_tensor=False)
276
+
277
+ def forward(self, tokens, mask=None):
278
+ return self.encoder(tokens, src_key_padding_mask=mask)
279
+
280
+
281
+ # ------------------------------------------------------------- future encoder
282
+ class FutureEncoder(nn.Module):
283
+ """modules.py::FutureEncoder -- encodes each agent's K modal futures into a
284
+ per-mode vector via max-pool over an MLP on [x,y,heading,vx,vy]. GameFormer
285
+ appends box size (w,l,h) -> dropped for NBA (8 -> 5 input dims)."""
286
+
287
+ def __init__(self, dim=D_MODEL, n_types=2, dt=0.2):
288
+ super().__init__()
289
+ self.dt = dt
290
+ self.mlp = nn.Sequential(nn.Linear(5, 64), nn.ReLU(), nn.Linear(64, dim))
291
+ self.type_emb = nn.Embedding(n_types, dim)
292
+
293
+ def state_process(self, trajs, cur_xy):
294
+ # trajs: [B, A, M, T, 2] ; cur_xy: [B, A, 2]
295
+ B, A, M, T, _ = trajs.shape
296
+ cur = cur_xy[:, :, None, None, :].expand(B, A, M, 1, 2)
297
+ xy = torch.cat([cur, trajs], dim=-2) # [B,A,M,T+1,2]
298
+ dxy = torch.diff(xy, dim=-2) # [B,A,M,T,2]
299
+ v = dxy / self.dt
300
+ theta = torch.atan2(dxy[..., 1], dxy[..., 0].clamp(min=1e-3)).unsqueeze(-1)
301
+ return torch.cat([trajs, theta, v], dim=-1) # [B,A,M,T,5]
302
+
303
+ def forward(self, trajs, cur_xy, types):
304
+ feat = self.state_process(trajs, cur_xy)
305
+ h = self.mlp(feat.detach()) # GameFormer detaches
306
+ out = torch.max(h, dim=-2).values # [B,A,M,D]
307
+ out = out + self.type_emb(types)[None, :, None, :] # + type emb
308
+ return out
309
+
310
+
311
+ # ----------------------------------------------------------- level-0 decoder
312
+ class InitialDecoder(nn.Module):
313
+ """modules.py::InitialDecoder -- vectorized over agents."""
314
+
315
+ def __init__(self, modalities, n_agents, future_len, dim=D_MODEL):
316
+ super().__init__()
317
+ self.M = modalities
318
+ self.A = n_agents
319
+ self.multi_modal_query_embedding = nn.Embedding(modalities, dim)
320
+ self.agent_query_embedding = nn.Embedding(n_agents, dim)
321
+ self.query_encoder = CrossTransformer(dim)
322
+ self.predictor = GMMPredictor(future_len, dim)
323
+ self.register_buffer('modal', torch.arange(modalities).long())
324
+ self.register_buffer('agent', torch.arange(n_agents).long())
325
+
326
+ def forward(self, encoding, cur_xy, mask=None):
327
+ # encoding: [B, A, D] (full agent context, shared as keys)
328
+ B, A, D = encoding.shape
329
+ M = self.M
330
+ multi_modal = self.multi_modal_query_embedding(self.modal) # [M, D]
331
+ agent = self.agent_query_embedding(self.agent) # [A, D]
332
+ mm_agent_query = multi_modal[None, :, :] + agent[:, None, :] # [A, M, D]
333
+ query = encoding[:, :, None, :] + mm_agent_query[None] # [B, A, M, D]
334
+
335
+ q = query.reshape(B * A, M, D)
336
+ kv = encoding[:, None, :, :].expand(B, A, A, D).reshape(B * A, A, D)
337
+ km = None
338
+ if mask is not None:
339
+ km = mask[:, None, :].expand(B, A, A).reshape(B * A, A)
340
+ content = self.query_encoder(q, kv, kv, km) # [B*A, M, D]
341
+ preds, scores = self.predictor(content) # [B*A,M,T,4],[B*A,M]
342
+
343
+ content = content.reshape(B, A, M, D)
344
+ T = preds.shape[-2]
345
+ preds = preds.reshape(B, A, M, T, 4)
346
+ scores = scores.reshape(B, A, M)
347
+ preds = preds.clone()
348
+ preds[..., :2] = preds[..., :2] + cur_xy[:, :, None, None, :]
349
+ return content, preds, scores
350
+
351
+
352
+ # ------------------------------------------------------- level-k decoder
353
+ class InteractionDecoder(nn.Module):
354
+ """modules.py::InteractionDecoder -- one game-theoretic reasoning level,
355
+ vectorized over agents. Conditions each agent on ALL agents' level-(k-1)
356
+ predictions (interaction self-attention), masking the agent's own future."""
357
+
358
+ def __init__(self, future_encoder, future_len, n_agents, dim=D_MODEL,
359
+ use_topn=None, use_sigma=None, use_relfut=None, top_n=None):
360
+ super().__init__()
361
+ self.A = n_agents
362
+ self.future_encoder = future_encoder # SHARED across levels
363
+ self.interaction_encoder = SelfTransformer(dim)
364
+ self.query_encoder = CrossTransformer(dim)
365
+ self.decoder = GMMPredictor(future_len, dim)
366
+ # --- SRA augmentations (read module-level toggles unless overridden) ---
367
+ self.use_topn = USE_TOPN if use_topn is None else use_topn
368
+ self.use_sigma = USE_SIGMA if use_sigma is None else use_sigma
369
+ self.use_relfut = USE_RELFUT if use_relfut is None else use_relfut
370
+ self.top_n = TOPN_K if top_n is None else top_n
371
+ if self.use_sigma: # uncertainty -> message gate in (0,1)
372
+ self.sigma_gate = nn.Sequential(
373
+ nn.Linear(1, 32), nn.ReLU(), nn.Linear(32, 1), nn.Sigmoid())
374
+ if self.use_relfut: # relational future edge encoder
375
+ self.rel_mlp = nn.Sequential(nn.Linear(4, 64), nn.ReLU(), nn.Linear(64, dim))
376
+
377
+ def forward(self, cur_xy, last_traj, last_scores, last_content, encoding, types, mask=None):
378
+ # cur_xy:[B,A,2] last_traj:[B,A,M,T,4] last_scores:[B,A,M]
379
+ # last_content:[B,A,M,D] encoding:[B,A,D]
380
+ B, A, M, T, _ = last_traj.shape
381
+ D = encoding.shape[-1]
382
+ dev = encoding.device
383
+ w = last_scores.softmax(-1) # [B,A,M] mode weights
384
+
385
+ # encode all agents' level-(k-1) modal futures
386
+ multi_futures = self.future_encoder(last_traj[..., :2], cur_xy, types) # [B,A,M,D]
387
+ futures = (multi_futures * w.unsqueeze(-1)).mean(dim=2) # [B,A,D]
388
+
389
+ # ---- SRA: per-agent uncertainty from prev-level GMM log-sigma ----------
390
+ sig_agent = None
391
+ if self.use_sigma:
392
+ sig = torch.exp(last_traj[..., 2:].clamp(0, 5)).mean(-1) # [B,A,M,T]
393
+ sig_agent = (sig * w[..., None]).sum(2).mean(-1) # [B,A] score-wtd, mean-T
394
+
395
+ # ---- SRA: sparse RAG neighbor selection from future proximity ----------
396
+ nbr_block = None # [B,A,A] True = block
397
+ rep = (last_traj[..., :2] * w[..., None, None]).sum(2) # [B,A,T,2] rep future
398
+ eye = torch.eye(A, dtype=torch.bool, device=dev)
399
+ if self.use_topn and A > 1:
400
+ prox = (rep[:, :, None] - rep[:, None]).norm(dim=-1).mean(-1) # [B,A,A]
401
+ if sig_agent is not None: # prefer confident neighbors
402
+ prox = prox + SIG_SEL_W * sig_agent[:, None, :]
403
+ prox = prox.masked_fill(eye[None], float('inf')) # exclude self from top-N
404
+ N = min(max(self.top_n - 1, 1), A - 1)
405
+ nbr_idx = prox.topk(N, largest=False, dim=-1).indices # [B,A,N] nearest others
406
+ nbr_block = torch.ones(B, A, A, dtype=torch.bool, device=dev)
407
+ nbr_block[:, torch.arange(A, device=dev), torch.arange(A, device=dev)] = False # self ok
408
+ nbr_block.scatter_(2, nbr_idx, False) # neighbors ok
409
+
410
+ # ---- SRA: uncertainty-gated message passing ----------------------------
411
+ if self.use_sigma:
412
+ futures = futures * self.sigma_gate(sig_agent[..., None]) # weaken uncertain senders
413
+
414
+ # ---- SRA: explicit relational future features --------------------------
415
+ if self.use_relfut and A > 1:
416
+ dpos = rep[:, :, None] - rep[:, None] # [B,A,A,T,2]
417
+ dvel = F.pad(torch.diff(dpos, dim=-2), (0, 0, 1, 0)) # [B,A,A,T,2]
418
+ relf = self.rel_mlp(torch.cat([dpos, dvel], dim=-1)).max(dim=-2).values # [B,A,A,D]
419
+ if nbr_block is not None:
420
+ relf = relf.masked_fill(nbr_block[..., None], 0.0)
421
+ denom = (~nbr_block).sum(-1, keepdim=True).clamp(min=1)
422
+ futures = futures + relf.sum(2) / denom # neighbor-mean relational future
423
+ else:
424
+ futures = futures + relf.mean(2)
425
+
426
+ # interaction over agents (self-attention; SRA restricts to neighbors)
427
+ attn_mask = None
428
+ if nbr_block is not None:
429
+ h = self.interaction_encoder.heads
430
+ attn_mask = nbr_block[:, None].expand(B, h, A, A).reshape(B * h, A, A)
431
+ interaction = self.interaction_encoder(futures, None, attn_mask) # [B,A,D]
432
+
433
+ # combined context = [interaction tokens ; context tokens]
434
+ ctx = torch.cat([interaction, encoding], dim=1) # [B, 2A, D]
435
+ # per-agent key mask: mask each agent's OWN future token in the 1st block
436
+ km = torch.zeros(B, A, 2 * A, dtype=torch.bool, device=dev)
437
+ idx = torch.arange(A, device=dev)
438
+ km[:, idx, idx] = True
439
+ if mask is not None:
440
+ # second block inherits context validity mask (all-valid for NBA)
441
+ km[:, :, A:] = km[:, :, A:] | mask[:, None, :]
442
+ km[:, :, :A] = km[:, :, :A] | mask[:, None, :]
443
+ if nbr_block is not None: # SRA sparsity on both blocks
444
+ km[:, :, :A] = km[:, :, :A] | nbr_block # interaction block: neighbors only
445
+ km[:, :, A:] = km[:, :, A:] | nbr_block # context block: neighbors(+self) only
446
+ km = km.reshape(B * A, 2 * A)
447
+ kv = ctx[:, None, :, :].expand(B, A, 2 * A, D).reshape(B * A, 2 * A, D)
448
+
449
+ query = (last_content + multi_futures).reshape(B * A, M, D) # prev content + own future
450
+ content = self.query_encoder(query, kv, kv, km) # [B*A, M, D]
451
+ traj, scores = self.decoder(content) # [B*A,M,T,4],[B*A,M]
452
+
453
+ content = content.reshape(B, A, M, D)
454
+ traj = traj.reshape(B, A, M, T, 4)
455
+ scores = scores.reshape(B, A, M)
456
+ traj = traj.clone()
457
+ traj[..., :2] = traj[..., :2] + cur_xy[:, :, None, None, :]
458
+ return content, traj, scores
459
+
460
+
461
+ # -------------------------------------------------------------- full decoder
462
+ class GameFormerDecoder(nn.Module):
463
+ """GameFormer.Decoder -- level 0 + L interaction levels (shared FutureEncoder)."""
464
+
465
+ def __init__(self, modalities, n_agents, future_len, levels, dim=D_MODEL,
466
+ n_types=2, dt=0.2):
467
+ super().__init__()
468
+ self._levels = levels
469
+ self.future_encoder = FutureEncoder(dim, n_types, dt)
470
+ self.initial_stage = InitialDecoder(modalities, n_agents, future_len, dim)
471
+ self.interaction_stage = nn.ModuleList(
472
+ [InteractionDecoder(self.future_encoder, future_len, n_agents, dim)
473
+ for _ in range(levels)])
474
+
475
+ def forward(self, encoding, cur_xy, types, mask=None):
476
+ out = {}
477
+ content, traj, scores = self.initial_stage(encoding, cur_xy, mask)
478
+ out['level_0_interactions'] = traj
479
+ out['level_0_scores'] = scores
480
+ for k in range(1, self._levels + 1):
481
+ content, traj, scores = self.interaction_stage[k - 1](
482
+ cur_xy, traj, scores, content, encoding, types, mask)
483
+ out[f'level_{k}_interactions'] = traj
484
+ out[f'level_{k}_scores'] = scores
485
+ return out
486
+
487
+
488
+ # ---------------------------------------------------------------- top model
489
+ class GameFormerNBA(nn.Module):
490
+ """GameFormer.GameFormer (NBA, map dropped)."""
491
+
492
+ def __init__(self, n_agents=11, past_dim=6, future_len=20, modalities=20,
493
+ levels=2, dim=D_MODEL, heads=N_HEADS, enc_layers=6,
494
+ n_types=2, dt=0.2, ball_idx=10):
495
+ super().__init__()
496
+ self.levels = levels
497
+ self.history_encoder = AgentHistoryEncoder(past_dim, dim, n_types)
498
+ self.fusion_encoder = FusionEncoder(dim, heads, enc_layers)
499
+ self.decoder = GameFormerDecoder(modalities, n_agents, future_len, levels,
500
+ dim, n_types, dt)
501
+ types = torch.zeros(n_agents, dtype=torch.long)
502
+ if 0 <= ball_idx < n_agents and n_types > 1:
503
+ types[ball_idx] = 1
504
+ self.register_buffer('agent_types', types)
505
+
506
+ def forward(self, feats, cur_xy):
507
+ # feats: [B,A,T,past_dim] (encoder input) ; cur_xy: [B,A,2] (metric frame)
508
+ enc = self.history_encoder(feats, self.agent_types) # [B,A,D]
509
+ enc = self.fusion_encoder(enc, None) # [B,A,D]
510
+ return self.decoder(enc, cur_xy, self.agent_types, None)
511
+
512
+
513
+ # ============================================================================
514
+ # LOSS (level_k_loss + gmm_loss, gmm=True; MARGINAL per agent for NBA)
515
+ # ============================================================================
516
+ def gmm_loss_marginal(traj, conv, scores, gt, metric_idx):
517
+ """Per-agent GMM winner-takes-all (marginal version of gmm_loss).
518
+ traj: [B,A,M,T,2] conv: [B,A,M,T,2] scores: [B,A,M] gt: [B,A,T,2]
519
+ """
520
+ B, A, M, T, _ = traj.shape
521
+ dist = torch.norm(traj - gt[:, :, None], dim=-1) # [B,A,M,T]
522
+ ndist = dist.mean(-1) + dist[..., metric_idx].sum(-1) # [B,A,M]
523
+ best = ndist.argmin(-1) # [B,A]
524
+
525
+ gi = best[..., None, None, None].expand(B, A, 1, T, 2)
526
+ best_traj = torch.gather(traj, 2, gi).squeeze(2) # [B,A,T,2]
527
+ best_conv = torch.gather(conv, 2, gi).squeeze(2) # [B,A,T,2]
528
+
529
+ dx = best_traj[..., 0] - gt[..., 0]
530
+ dy = best_traj[..., 1] - gt[..., 1]
531
+ log_std_x = torch.clip(best_conv[..., 0], 0, 5)
532
+ log_std_y = torch.clip(best_conv[..., 1], 0, 5)
533
+ std_x, std_y = torch.exp(log_std_x), torch.exp(log_std_y)
534
+
535
+ reg = (log_std_x + log_std_y) + 0.5 * ((dx ** 2) / (std_x ** 2) + (dy ** 2) / (std_y ** 2))
536
+ reg = reg.mean(-1) + reg[..., metric_idx].sum(-1) # [B,A]
537
+
538
+ prob_loss = F.cross_entropy(scores.reshape(B * A, M), best.reshape(B * A),
539
+ label_smoothing=0.2)
540
+ return reg.mean() + 2.0 * prob_loss, best
541
+
542
+
543
+ def level_k_loss_marginal(outputs, gt_center, levels, metric_idx):
544
+ """Sum GMM loss across all levels 0..L (GameFormer's level-wise imitation)."""
545
+ total = 0.0
546
+ for k in range(levels + 1):
547
+ pred = outputs[f'level_{k}_interactions'] # [B,A,M,T,4]
548
+ scores = outputs[f'level_{k}_scores'] # [B,A,M]
549
+ traj, conv = pred[..., :2], pred[..., 2:]
550
+ l, _ = gmm_loss_marginal(traj, conv, scores, gt_center, metric_idx)
551
+ total = total + l
552
+ return total
553
+
554
+
555
+ # ============================================================================
556
+ # DATA
557
+ # ============================================================================
558
+ def build_loaders(args):
559
+ cfg = _types.SimpleNamespace(
560
+ traj_mean=[14, 7.5], data_norm='min_max',
561
+ past_frames=args.past_len, future_frames=args.future_len, agents=args.agents)
562
+ # Train first so cfg min/max scalars are populated for the test set.
563
+ train_set = NBADatasetMinMax(
564
+ obs_len=args.past_len, pred_len=args.future_len, training=True,
565
+ num_scenes=args.n_train, cfg=cfg, data_dir=args.data_dir,
566
+ rotate=False, data_norm='min_max')
567
+ test_set = NBADatasetMinMax(
568
+ obs_len=args.past_len, pred_len=args.future_len, training=False,
569
+ test_scenes=args.n_test, cfg=cfg, data_dir=args.data_dir,
570
+ rotate=False, data_norm='min_max')
571
+ train_loader = DataLoader(
572
+ train_set, batch_size=args.batch_size, shuffle=True, num_workers=args.workers,
573
+ collate_fn=seq_collate_nba, pin_memory=True, drop_last=True)
574
+ test_loader = DataLoader(
575
+ test_set, batch_size=args.test_batch, shuffle=False, num_workers=args.workers,
576
+ collate_fn=seq_collate_nba, pin_memory=True)
577
+ return train_loader, test_loader
578
+
579
+
580
+ def unpack(data, device):
581
+ feats = data['past_traj'].to(device) # [B,A,T,6] normalized
582
+ past_orig = data['past_traj_original_scale'].to(device) # [B,A,T,6] metric
583
+ gt_disp = data['fut_traj_original_scale'].to(device) # [B,A,Tf,2] displacement
584
+ cur_xy = past_orig[:, :, -1, 0:2] # centered-abs last pos
585
+ gt_center = gt_disp + cur_xy[:, :, None, :] # centered-abs future
586
+ return feats, cur_xy, gt_disp, gt_center
587
+
588
+
589
+ # ============================================================================
590
+ # EVAL (marginal min-of-K=20 at 4.0 s; identical to eval_perscene_moflow.py)
591
+ # ============================================================================
592
+ @torch.no_grad()
593
+ def evaluate(model, loader, device, levels, end):
594
+ model.eval()
595
+ ade_sum = fde_sum = 0.0
596
+ n = 0
597
+ for data in loader:
598
+ feats, cur_xy, gt_disp, _ = unpack(data, device)
599
+ out = model(feats, cur_xy)
600
+ pred = out[f'level_{levels}_interactions'][..., :2] # [B,A,M,T,2]
601
+ pred_disp = pred - cur_xy[:, :, None, None, :] # -> displacement
602
+ d = torch.norm(pred_disp - gt_disp[:, :, None], dim=-1) # [B,A,M,T]
603
+ ade = d[..., :end].mean(-1).min(dim=-1).values # [B,A] best-of-K
604
+ fde = d[..., end - 1].min(dim=-1).values # [B,A]
605
+ ade_sum += ade.sum().item()
606
+ fde_sum += fde.sum().item()
607
+ n += ade.numel()
608
+ return ade_sum / max(n, 1), fde_sum / max(n, 1)
609
+
610
+
611
+ # ============================================================================
612
+ # TRAIN
613
+ # ============================================================================
614
+ def parse_args():
615
+ p = argparse.ArgumentParser('GameFormer standalone predictor for NBA')
616
+ p.add_argument('--data_dir', default='/mnt/jaewoo4tb/srtp/MoFlow/data/nba', type=str)
617
+ p.add_argument('--gpu', default='0', type=str)
618
+ p.add_argument('--exp', default='sragf_nba', type=str)
619
+ p.add_argument('--epochs', default=50, type=int)
620
+ p.add_argument('--batch_size', default=128, type=int)
621
+ p.add_argument('--test_batch', default=500, type=int)
622
+ p.add_argument('--workers', default=4, type=int)
623
+ p.add_argument('--seed', default=3407, type=int) # GameFormer default
624
+
625
+ # --- architecture (faithful GameFormer knobs) ---
626
+ p.add_argument('--modalities', default=20, type=int, help='K modes (NBA min-ADE_20)')
627
+ p.add_argument('--level', default=2, type=int, help='game-theoretic levels (paper: 3)')
628
+ p.add_argument('--encoder_layers', default=6, type=int)
629
+ p.add_argument('--agents', default=11, type=int)
630
+ p.add_argument('--past_len', default=10, type=int)
631
+ p.add_argument('--future_len', default=20, type=int) # 4.0 s @ 5 Hz
632
+ p.add_argument('--ball_idx', default=10, type=int, help='-1 to disable type emb')
633
+
634
+ # --- optimization (GameFormer: AdamW 1e-4, MultiStepLR x0.5, clip 5) ---
635
+ p.add_argument('--lr', default=1e-4, type=float)
636
+ p.add_argument('--weight_decay', default=1e-4, type=float)
637
+ p.add_argument('--grad_clip', default=5.0, type=float)
638
+ p.add_argument('--n_train', default=32500, type=int)
639
+ p.add_argument('--n_test', default=12500, type=int)
640
+ p.add_argument('--eval_every', default=2, type=int)
641
+ p.add_argument('--save_dir', default=os.path.join(
642
+ os.path.dirname(os.path.abspath(__file__)), 'sragf_nba_ckpt'), type=str)
643
+ return p.parse_args()
644
+
645
+
646
+ def main():
647
+ args = parse_args()
648
+ torch.manual_seed(args.seed)
649
+ np.random.seed(args.seed)
650
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
651
+
652
+ train_loader, test_loader = build_loaders(args)
653
+
654
+ n_types = 2 if (0 <= args.ball_idx < args.agents) else 1
655
+ model = GameFormerNBA(
656
+ n_agents=args.agents, past_dim=6, future_len=args.future_len,
657
+ modalities=args.modalities, levels=args.level, dim=D_MODEL, heads=N_HEADS,
658
+ enc_layers=args.encoder_layers, n_types=n_types, dt=0.2,
659
+ ball_idx=args.ball_idx).to(device)
660
+ n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
661
+ print(f'[SRA-GF] model params: {n_params/1e6:.2f} M | levels={args.level} '
662
+ f'K={args.modalities} enc_layers={args.encoder_layers} device={device}')
663
+ print(f'[SRA-GF] SRA components: neighbor-topN={USE_TOPN}(N={TOPN_K}) '
664
+ f'sigma-gate={USE_SIGMA} relational-future={USE_RELFUT} '
665
+ f'(all False == vanilla GameFormer)', flush=True)
666
+
667
+ opt = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
668
+ milestones = sorted(set(int(args.epochs * f) for f in (0.5, 0.65, 0.8, 0.9)))
669
+ milestones = [m for m in milestones if 0 < m < args.epochs]
670
+ sched = torch.optim.lr_scheduler.MultiStepLR(opt, milestones=milestones, gamma=0.5)
671
+
672
+ end = args.future_len # 4.0 s = frame 20
673
+ metric_idx = sorted(set([max(0, args.future_len // 2 - 1), args.future_len - 1]))
674
+
675
+ os.makedirs(args.save_dir, exist_ok=True)
676
+ best_ade = float('inf')
677
+
678
+ for epoch in range(args.epochs):
679
+ model.train()
680
+ t0 = time.time()
681
+ running = 0.0
682
+ nb = 0
683
+ for data in train_loader:
684
+ feats, cur_xy, _, gt_center = unpack(data, device)
685
+ out = model(feats, cur_xy)
686
+ loss = level_k_loss_marginal(out, gt_center, args.level, metric_idx)
687
+ opt.zero_grad()
688
+ loss.backward()
689
+ torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
690
+ opt.step()
691
+ running += float(loss.item())
692
+ nb += 1
693
+ sched.step()
694
+ print(f'[SRA-GF] epoch {epoch+1}/{args.epochs} loss={running/max(nb,1):.4f} '
695
+ f'lr={opt.param_groups[0]["lr"]:.2e} ({time.time()-t0:.1f}s)', flush=True)
696
+
697
+ if (epoch + 1) % args.eval_every == 0 or epoch == args.epochs - 1:
698
+ ade, fde = evaluate(model, test_loader, device, args.level, end)
699
+ print(f'[SRA-GF] epoch {epoch+1} ADE4={ade:.4f} FDE4={fde:.4f}', flush=True)
700
+ if ade < best_ade:
701
+ best_ade = ade
702
+ torch.save({'model': model.state_dict(), 'epoch': epoch + 1,
703
+ 'ade4': ade, 'fde4': fde, 'args': vars(args)},
704
+ os.path.join(args.save_dir, f'{args.exp}_best.pt'))
705
+ print(f'[SRA-GF] saved best (ADE4={ade:.4f}) -> '
706
+ f'{os.path.join(args.save_dir, args.exp + "_best.pt")}', flush=True)
707
+
708
+ print(f'[SRA-GF] done. best ADE4={best_ade:.4f}', flush=True)
709
+
710
+
711
+ if __name__ == '__main__':
712
+ main()