OutOfMystic Claude Opus 4.6 commited on
Commit
b61d866
·
1 Parent(s): b055f87

Rewrite notebook: per-piece GRPO training on Qwen 3B

Browse files

Complete rewrite of training approach:
- Model sees board before EVERY piece (not 100 blind actions)
- Generates up to 20 action tokens per piece, stops when piece locks
- Custom REINFORCE/GRPO loop (no GRPOTrainer dependency)
- Two-phase: rollout without grad, then recompute log_probs with grad
- 8 games per iteration (same seed, GRPO-style comparison)
- 100 iterations with different seeds

Model changes:
- Qwen2.5-3B-Instruct (was 7B)
- LoRA r=16 (was r=32)
- Target GPU: L4 (was A100)

Reward structure:
- Engine per-step rewards summed across entire game
- L/R penalty: -0.1 per move
- Piece placed bonus: +1.0
- No-place penalty: -10.0 (forced drop after 20 tokens)
- Line clear bonus: +100 per line

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

docs/plans/2026-03-09-per-piece-training-design.md ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Per-Piece Tetris Training Design
2
+
3
+ ## Overview
4
+
5
+ Redesign training so the model sees the board before EVERY piece placement,
6
+ instead of outputting 100 blind actions from a single prompt.
7
+
8
+ ## Key Parameters
9
+
10
+ - **Model**: Qwen2.5-3B-Instruct + LoRA
11
+ - **GPU**: L4
12
+ - **Games per iteration**: 8 (same seed, GRPO-style comparison)
13
+ - **Max steps per game**: 200 (total actions across all pieces)
14
+ - **Max tokens per piece**: 20 (if piece not placed -> forced drop + penalty)
15
+ - **Training iterations**: 100 (each with a new seed)
16
+ - **No history**: each model call = fresh prompt with current board only
17
+
18
+ ## One Model Call (one piece)
19
+
20
+ 1. Build prompt: system message + current board + current piece + next piece
21
+ 2. Model generates up to 20 tokens (L/R/C/W/D/S)
22
+ 3. Play actions one by one on the engine
23
+ 4. Stop when piece locks (new piece spawns) or 20 tokens exhausted
24
+ 5. If 20 tokens used and piece NOT placed: forced drop + penalty (-10)
25
+
26
+ ### How to detect piece lock
27
+
28
+ After each `env.step()`, check if `current_piece` changed from the one
29
+ shown in the prompt. If it changed -> piece was placed, stop processing tokens.
30
+
31
+ ## One Game (one playthrough)
32
+
33
+ ```
34
+ seed = iteration_seed
35
+ env.reset(seed)
36
+ total_reward = 0
37
+ all_log_probs = []
38
+ steps = 0
39
+
40
+ while not game_over and steps < 200:
41
+ board_state = env.get_state()
42
+ current_piece = board_state['current_piece']
43
+
44
+ prompt = build_prompt(board_state) # fresh each time, no history
45
+ tokens, log_probs = model_generate(prompt, max_tokens=20)
46
+
47
+ piece_placed = False
48
+ for token in tokens:
49
+ action = token_to_action(token)
50
+ result = env.step(action)
51
+ total_reward += result['reward']
52
+ total_reward -= 0.1 if action in ('left', 'right') else 0
53
+ all_log_probs.append(log_probs[token])
54
+ steps += 1
55
+
56
+ if result['current_piece'] != current_piece:
57
+ # Piece was placed, new piece spawned
58
+ piece_placed = True
59
+ total_reward += 1.0 # bonus for placing piece
60
+ break
61
+
62
+ if result['done']:
63
+ game_over = True
64
+ break
65
+
66
+ if not piece_placed and not game_over:
67
+ # Piece not placed in 20 tokens -> force drop + penalty
68
+ env.step('drop')
69
+ total_reward -= 10.0
70
+ steps += 1
71
+ ```
72
+
73
+ ## Reward Structure
74
+
75
+ Per-step rewards from game engine (summed across ALL steps):
76
+ - Step penalty: -1 per step
77
+ - Line clears: +100/+300/+700/+1500 (1/2/3/4 lines)
78
+ - Height penalty: -2 * max_height (per step)
79
+ - Hole penalty: -5 * holes (per step)
80
+ - Game over: -500
81
+
82
+ Additional rewards added by training loop:
83
+ - L or R move: -0.1 (discourages aimless shuffling)
84
+ - Piece placed: +1.0 (encourages completing placements)
85
+ - Piece NOT placed in 20 tokens: -10.0 (forces learning to drop)
86
+ - Line clear bonus: +100 per line (1 line=+100, 2=+200, 3=+300, 4=+400)
87
+
88
+ Total game reward = sum of all the above across entire game.
89
+
90
+ ## One Training Iteration
91
+
92
+ ```
93
+ seed = random_seed_for_this_iteration
94
+
95
+ # Play 8 games with same seed
96
+ rewards = []
97
+ all_game_log_probs = []
98
+
99
+ for game_idx in range(8):
100
+ reward, log_probs_sum = play_one_game(model, seed)
101
+ rewards.append(reward)
102
+ all_game_log_probs.append(log_probs_sum)
103
+
104
+ # GRPO-style advantage
105
+ rewards = torch.tensor(rewards)
106
+ advantages = (rewards - rewards.mean()) / (rewards.std() + 1e-8)
107
+
108
+ # Policy gradient loss
109
+ loss = 0
110
+ for i in range(8):
111
+ loss -= all_game_log_probs[i] * advantages[i]
112
+ loss = loss / 8
113
+
114
+ loss.backward()
115
+ optimizer.step()
116
+ optimizer.zero_grad()
117
+ ```
118
+
119
+ ## Full Training
120
+
121
+ ```
122
+ for iteration in range(100):
123
+ seed = iteration # deterministic but different each time
124
+ train_one_iteration(model, seed)
125
+
126
+ if iteration % 10 == 0:
127
+ print(f"Iter {iteration}: avg_reward={mean}, std={std}")
128
+ ```
129
+
130
+ ## Action Token Mapping
131
+
132
+ Pre-compute token IDs for action characters:
133
+ ```python
134
+ ACTION_TOKENS = {
135
+ tokenizer.encode('L', add_special_tokens=False)[0]: 'left',
136
+ tokenizer.encode('R', add_special_tokens=False)[0]: 'right',
137
+ tokenizer.encode('C', add_special_tokens=False)[0]: 'rotate_cw',
138
+ tokenizer.encode('W', add_special_tokens=False)[0]: 'rotate_ccw',
139
+ tokenizer.encode('D', add_special_tokens=False)[0]: 'drop',
140
+ tokenizer.encode('S', add_special_tokens=False)[0]: 'down',
141
+ }
142
+ ```
143
+
144
+ During generation: mask logits to only allow these 6 tokens.
145
+ Sample from softmax over 6 logits -> get action + log_prob.
146
+
147
+ ## Notebook Cell Structure
148
+
149
+ 1. Install deps (peft, trl, accelerate, etc.)
150
+ 2. Load Qwen2.5-3B-Instruct + LoRA
151
+ 3. Download game_engine.py, define prompt builder
152
+ 4. **Demo: untrained model plays one game** (show board after each piece)
153
+ 5. Define `play_one_game()` and `train_one_iteration()`
154
+ 6. Training loop (100 iterations)
155
+ 7. Plot reward curve
156
+ 8. **Demo: trained model plays one game** (compare with untrained)
157
+ 9. Push model to HF Hub
158
+
159
+ ## Time Estimate
160
+
161
+ - 3B model forward pass: ~0.05s per token on T4
162
+ - Per piece: ~20 tokens * 0.05s = ~1s (forward passes)
163
+ - Per game: ~30-50 pieces * 1s = ~40s
164
+ - Per iteration: 8 games (sequential) = ~320s OR batched = ~40-80s
165
+ - 100 iterations: ~70-130 min
166
+
167
+ Fits in a Colab T4 session (4h limit).
168
+
169
+ Note: batching 8 games in parallel requires handling variable-length episodes.
170
+ Simpler approach: run 8 games sequentially (~5 min/iteration, ~8h total).
171
+ Better approach: batch the forward passes across 8 games at each "step",
172
+ masking out finished games.
docs/plans/2026-03-09-per-piece-training-plan.md ADDED
@@ -0,0 +1,580 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Per-Piece Tetris GRPO Training — Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Rewrite the Colab notebook so the model sees the board before every piece, plays actions until piece locks, and learns via GRPO-style policy gradient over full games.
6
+
7
+ **Architecture:** Custom REINFORCE/GRPO loop (no GRPOTrainer). Two-phase per iteration: (1) rollout 8 games without grad, storing (prompt, actions) pairs; (2) recompute log_probs with grad, multiply by advantage, backward. This keeps memory low — only one piece's activations in memory at a time.
8
+
9
+ **Tech Stack:** transformers, peft (LoRA), torch, game_engine.py (local Tetris)
10
+
11
+ ---
12
+
13
+ ### Task 1: Cell 1 — Install Dependencies
14
+
15
+ **Files:**
16
+ - Modify: `tetris_training.ipynb` Cell 1
17
+
18
+ **Step 1: Write cell content**
19
+
20
+ ```python
21
+ # Cell 1: Install dependencies
22
+ !pip install peft accelerate -q
23
+ ```
24
+
25
+ Note: we no longer need `trl` (no GRPOTrainer), `openenv-core`, or `datasets`.
26
+
27
+ **Step 2: Run cell, verify no errors**
28
+
29
+ **Step 3: Commit**
30
+
31
+ ```bash
32
+ git add tetris_training.ipynb
33
+ git commit -m "cell 1: minimal deps for custom training loop"
34
+ ```
35
+
36
+ ---
37
+
38
+ ### Task 2: Cell 2 — Load Model + LoRA
39
+
40
+ **Files:**
41
+ - Modify: `tetris_training.ipynb` Cell 2
42
+
43
+ **Step 1: Write cell content**
44
+
45
+ ```python
46
+ # Cell 2: Load Qwen2.5-3B-Instruct + LoRA
47
+ import torch
48
+ from transformers import AutoModelForCausalLM, AutoTokenizer
49
+ from peft import LoraConfig, get_peft_model
50
+
51
+ model_name = "Qwen/Qwen2.5-3B-Instruct"
52
+
53
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
54
+ if tokenizer.pad_token is None:
55
+ tokenizer.pad_token = tokenizer.eos_token
56
+
57
+ model = AutoModelForCausalLM.from_pretrained(
58
+ model_name,
59
+ torch_dtype=torch.bfloat16,
60
+ device_map="auto",
61
+ )
62
+
63
+ lora_config = LoraConfig(
64
+ r=16,
65
+ lora_alpha=16,
66
+ lora_dropout=0,
67
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
68
+ "gate_proj", "up_proj", "down_proj"],
69
+ task_type="CAUSAL_LM",
70
+ )
71
+
72
+ model = get_peft_model(model, lora_config)
73
+ model.print_trainable_parameters()
74
+ ```
75
+
76
+ Note: 3B model, r=16 (smaller than 7B's r=32), bf16.
77
+
78
+ **Step 2: Run cell, verify output shows trainable params**
79
+
80
+ **Step 3: Commit**
81
+
82
+ ---
83
+
84
+ ### Task 3: Cell 3 — Game Engine + Constants + Prompt Builder
85
+
86
+ **Files:**
87
+ - Modify: `tetris_training.ipynb` Cell 3 (replaces old TetrisClient cell)
88
+
89
+ **Step 1: Write cell content**
90
+
91
+ ```python
92
+ # Cell 3: Game engine + constants + prompt builder
93
+ import random
94
+ import torch.nn.functional as F
95
+
96
+ # Download game engine
97
+ !wget -q -O game_engine.py https://raw.githubusercontent.com/OutOfMystic/tetris-openenv/main/src/tetris_env/server/game_engine.py
98
+ from game_engine import TetrisEnv
99
+
100
+ # === Constants ===
101
+ MAX_ACTIONS_PER_PIECE = 20
102
+ MAX_STEPS_PER_GAME = 200
103
+ GAMES_PER_ITER = 8
104
+ NUM_ITERATIONS = 100
105
+ TEMPERATURE = 0.7
106
+
107
+ # Training reward modifiers (on top of engine rewards)
108
+ LR_PENALTY = -0.1 # per L/R move
109
+ PIECE_PLACED_BONUS = 1.0 # per piece successfully placed
110
+ NO_PLACE_PENALTY = -10.0 # if 20 tokens exhausted without placing
111
+ LINE_CLEAR_BONUS = 100.0 # per line cleared (1=+100, 2=+200, 3=+300, 4=+400)
112
+
113
+ # Action mapping
114
+ ACTION_CHARS = ['L', 'R', 'C', 'W', 'D', 'S']
115
+ ACTION_TO_ENGINE = {
116
+ 'L': 'left', 'R': 'right', 'C': 'rotate_cw',
117
+ 'W': 'rotate_ccw', 'D': 'drop', 'S': 'down'
118
+ }
119
+
120
+ # Pre-compute token IDs for action characters
121
+ ACTION_TOKEN_IDS = []
122
+ for ch in ACTION_CHARS:
123
+ ids = tokenizer.encode(ch, add_special_tokens=False)
124
+ ACTION_TOKEN_IDS.append(ids[0])
125
+ print(f" '{ch}' -> token_id {ids[0]}")
126
+ ACTION_TOKEN_IDS = torch.tensor(ACTION_TOKEN_IDS, device=model.device)
127
+
128
+ # Token ID -> action char lookup
129
+ TOKEN_TO_CHAR = {ACTION_TOKEN_IDS[i].item(): ACTION_CHARS[i] for i in range(6)}
130
+
131
+ SYSTEM_PROMPT = """You are a Tetris AI. You see the board and current piece.
132
+ Output actions as single letters: L=left R=right C=rotate_cw W=rotate_ccw D=drop S=down
133
+ Place the piece to fill complete rows. Drop when positioned."""
134
+
135
+ def build_prompt(result):
136
+ return f"""Board:
137
+ {result['board']}
138
+
139
+ Piece: {result['current_piece']} Next: {result['next_piece']}
140
+ Score: {result['score']} Lines: {result['total_lines']} Height: {result['max_height']} Holes: {result['holes']}
141
+
142
+ Your actions:"""
143
+
144
+ print("Game engine loaded. Action tokens mapped.")
145
+ ```
146
+
147
+ **Step 2: Run cell, verify 6 token mappings printed**
148
+
149
+ **Step 3: Commit**
150
+
151
+ ---
152
+
153
+ ### Task 4: Cell 4 — Core Functions (play_one_game + train_one_iteration)
154
+
155
+ **Files:**
156
+ - Modify: `tetris_training.ipynb` Cell 4
157
+
158
+ This is the most critical cell. Two phases per iteration:
159
+ - **Rollout** (no grad): play 8 games, store (prompt_ids, action_token_ids) per piece
160
+ - **Update** (with grad): recompute log_probs, multiply by advantage, backward per piece
161
+
162
+ **Step 1: Write cell content**
163
+
164
+ ```python
165
+ # Cell 4: Core training functions
166
+
167
+ def play_one_game(model, tokenizer, seed, temperature=TEMPERATURE):
168
+ """
169
+ Play a full Tetris game. Model sees board before each piece,
170
+ generates up to 20 action tokens per piece.
171
+ Returns reward and piece data for gradient computation.
172
+ """
173
+ env = TetrisEnv(seed=seed)
174
+ env.reset(seed=seed)
175
+
176
+ # Initial random offset (same as old prompt generation)
177
+ rng = random.Random(seed)
178
+ moves = rng.randint(0, 4)
179
+ direction = rng.choice(["left", "right"])
180
+ for _ in range(moves):
181
+ if env.done:
182
+ break
183
+ env.step(direction)
184
+
185
+ total_reward = 0.0
186
+ total_steps = 0
187
+ pieces_data = []
188
+
189
+ while not env.done and total_steps < MAX_STEPS_PER_GAME:
190
+ current_piece_name = env.current_piece_name
191
+ lines_before = env.total_lines
192
+
193
+ # Build fresh prompt for this piece
194
+ result = env._make_result(0)
195
+ messages = [
196
+ {"role": "system", "content": SYSTEM_PROMPT},
197
+ {"role": "user", "content": build_prompt(result)},
198
+ ]
199
+ prompt_ids = tokenizer.apply_chat_template(
200
+ messages, return_tensors="pt", add_generation_prompt=True
201
+ ).to(model.device)
202
+
203
+ # Autoregressive generation with KV cache (no grad)
204
+ input_ids = prompt_ids
205
+ action_ids_list = []
206
+ piece_placed = False
207
+ past_kv = None
208
+
209
+ with torch.no_grad():
210
+ for _ in range(MAX_ACTIONS_PER_PIECE):
211
+ if past_kv is None:
212
+ out = model(input_ids, use_cache=True)
213
+ past_kv = out.past_key_values
214
+ else:
215
+ out = model(input_ids[:, -1:], past_key_values=past_kv, use_cache=True)
216
+ past_kv = out.past_key_values
217
+
218
+ logits = out.logits[:, -1, :] # [1, vocab]
219
+
220
+ # Mask to only 6 action tokens
221
+ masked = torch.full_like(logits, float('-inf'))
222
+ masked[0, ACTION_TOKEN_IDS] = logits[0, ACTION_TOKEN_IDS]
223
+ probs = F.softmax(masked / temperature, dim=-1)
224
+
225
+ # Sample
226
+ token_id = torch.multinomial(probs, 1).item()
227
+ action_ids_list.append(token_id)
228
+
229
+ # Execute in engine
230
+ action_char = TOKEN_TO_CHAR[token_id]
231
+ action_name = ACTION_TO_ENGINE[action_char]
232
+ step_result = env.step(action_name)
233
+
234
+ total_reward += step_result['reward']
235
+ total_steps += 1
236
+
237
+ if action_name in ('left', 'right'):
238
+ total_reward += LR_PENALTY
239
+
240
+ # Check if piece placed (new piece spawned)
241
+ if env.current_piece_name != current_piece_name:
242
+ piece_placed = True
243
+ total_reward += PIECE_PLACED_BONUS
244
+ lines_cleared = env.total_lines - lines_before
245
+ if lines_cleared > 0:
246
+ total_reward += lines_cleared * LINE_CLEAR_BONUS
247
+ break
248
+
249
+ if env.done:
250
+ break
251
+
252
+ # Append token for next autoregressive step
253
+ input_ids = torch.cat([
254
+ input_ids,
255
+ torch.tensor([[token_id]], device=model.device)
256
+ ], dim=-1)
257
+
258
+ # Force drop if piece not placed
259
+ if not piece_placed and not env.done:
260
+ env.step('drop')
261
+ total_reward += NO_PLACE_PENALTY
262
+ total_steps += 1
263
+ lines_cleared = env.total_lines - lines_before
264
+ if lines_cleared > 0:
265
+ total_reward += lines_cleared * LINE_CLEAR_BONUS
266
+
267
+ # Store for gradient computation
268
+ if action_ids_list:
269
+ pieces_data.append({
270
+ 'prompt_ids': prompt_ids.cpu(),
271
+ 'action_ids': torch.tensor(action_ids_list, dtype=torch.long),
272
+ })
273
+
274
+ return {
275
+ 'reward': total_reward,
276
+ 'pieces': pieces_data,
277
+ 'total_steps': total_steps,
278
+ 'total_lines': env.total_lines,
279
+ 'pieces_placed': len(pieces_data),
280
+ }
281
+
282
+
283
+ def train_one_iteration(model, optimizer, seed, temperature=TEMPERATURE):
284
+ """
285
+ One GRPO iteration:
286
+ 1. Play 8 games (same seed) without grad
287
+ 2. Compute advantages
288
+ 3. Recompute log_probs with grad, apply policy gradient
289
+ """
290
+ # Phase 1: Rollout
291
+ games = []
292
+ for _ in range(GAMES_PER_ITER):
293
+ game = play_one_game(model, tokenizer, seed, temperature)
294
+ games.append(game)
295
+
296
+ rewards = torch.tensor([g['reward'] for g in games], dtype=torch.float32)
297
+ mean_r = rewards.mean().item()
298
+ std_r = rewards.std().item()
299
+
300
+ # Phase 2: Advantages (GRPO-style)
301
+ if std_r < 1e-8:
302
+ # All games got same reward — no learning signal
303
+ return {'mean_reward': mean_r, 'std_reward': 0.0, 'loss': 0.0,
304
+ 'avg_steps': sum(g['total_steps'] for g in games) / GAMES_PER_ITER,
305
+ 'avg_lines': sum(g['total_lines'] for g in games) / GAMES_PER_ITER}
306
+
307
+ advantages = ((rewards - rewards.mean()) / (rewards.std() + 1e-8)).tolist()
308
+
309
+ # Phase 3: Update — recompute log_probs with grad
310
+ optimizer.zero_grad()
311
+ total_pieces = sum(len(g['pieces']) for g in games)
312
+ total_loss = 0.0
313
+
314
+ for game_idx, game in enumerate(games):
315
+ adv = advantages[game_idx]
316
+ for piece in game['pieces']:
317
+ prompt = piece['prompt_ids'].to(model.device)
318
+ actions = piece['action_ids'].to(model.device)
319
+ if len(actions) == 0:
320
+ continue
321
+
322
+ # Teacher-forced forward pass: prompt + actions
323
+ full_input = torch.cat([prompt.squeeze(0), actions]).unsqueeze(0)
324
+ logits = model(full_input).logits
325
+
326
+ # Log_probs at positions where actions were generated
327
+ P = prompt.shape[-1]
328
+ action_logits = logits[0, P-1 : P-1+len(actions), :]
329
+
330
+ # Mask to action tokens, apply temperature
331
+ masked = torch.full_like(action_logits, float('-inf'))
332
+ masked[:, ACTION_TOKEN_IDS] = action_logits[:, ACTION_TOKEN_IDS]
333
+ log_probs = F.log_softmax(masked / temperature, dim=-1)
334
+
335
+ selected = log_probs.gather(1, actions.unsqueeze(1)).squeeze(1)
336
+ piece_loss = -(selected.sum() * adv) / total_pieces
337
+
338
+ piece_loss.backward()
339
+ total_loss += piece_loss.item()
340
+
341
+ # Gradient clipping
342
+ torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
343
+ optimizer.step()
344
+
345
+ return {
346
+ 'mean_reward': mean_r,
347
+ 'std_reward': std_r,
348
+ 'loss': total_loss,
349
+ 'avg_steps': sum(g['total_steps'] for g in games) / GAMES_PER_ITER,
350
+ 'avg_lines': sum(g['total_lines'] for g in games) / GAMES_PER_ITER,
351
+ 'avg_pieces': sum(g['pieces_placed'] for g in games) / GAMES_PER_ITER,
352
+ }
353
+
354
+ print("Training functions defined.")
355
+ ```
356
+
357
+ **Step 2: Run cell, verify "Training functions defined." printed**
358
+
359
+ **Step 3: Quick smoke test**
360
+
361
+ ```python
362
+ # Smoke test: play 1 game
363
+ test_game = play_one_game(model, tokenizer, seed=0)
364
+ print(f"Reward: {test_game['reward']:.1f}, Steps: {test_game['total_steps']}, "
365
+ f"Pieces: {test_game['pieces_placed']}, Lines: {test_game['total_lines']}")
366
+ ```
367
+
368
+ **Step 4: Commit**
369
+
370
+ ---
371
+
372
+ ### Task 5: Cell 5 — Demo Untrained Model
373
+
374
+ **Files:**
375
+ - Modify: `tetris_training.ipynb` Cell 5
376
+
377
+ **Step 1: Write cell content**
378
+
379
+ ```python
380
+ # Cell 5: Demo — UNTRAINED model plays one game
381
+ print("=== UNTRAINED MODEL ===\n")
382
+
383
+ game = play_one_game(model, tokenizer, seed=42)
384
+
385
+ print(f"Total steps: {game['total_steps']}")
386
+ print(f"Pieces placed: {game['pieces_placed']}")
387
+ print(f"Lines cleared: {game['total_lines']}")
388
+ print(f"Game reward: {game['reward']:+.1f}")
389
+
390
+ # Show final board
391
+ env = TetrisEnv(seed=42)
392
+ env.reset(seed=42)
393
+ rng = random.Random(42)
394
+ moves = rng.randint(0, 4)
395
+ direction = rng.choice(["left", "right"])
396
+ for _ in range(moves):
397
+ env.step(direction)
398
+
399
+ # Replay the actions to get final board
400
+ for piece in game['pieces']:
401
+ for token_id in piece['action_ids'].tolist():
402
+ action_name = ACTION_TO_ENGINE[TOKEN_TO_CHAR[token_id]]
403
+ if not env.done:
404
+ env.step(action_name)
405
+ if not env.done:
406
+ # Check if piece was placed; if not, force drop
407
+ pass # engine handles lock internally
408
+
409
+ print(f"\nFinal board:")
410
+ print(env.board_to_text())
411
+
412
+ untrained_reward = game['reward']
413
+ ```
414
+
415
+ **Step 2: Run cell, verify board prints**
416
+
417
+ **Step 3: Commit**
418
+
419
+ ---
420
+
421
+ ### Task 6: Cell 6 — Training Loop
422
+
423
+ **Files:**
424
+ - Modify: `tetris_training.ipynb` Cell 6
425
+
426
+ **Step 1: Write cell content**
427
+
428
+ ```python
429
+ # Cell 6: Training loop — 100 iterations
430
+ optimizer = torch.optim.AdamW(
431
+ [p for p in model.parameters() if p.requires_grad],
432
+ lr=5e-6,
433
+ weight_decay=0.01,
434
+ )
435
+
436
+ history = []
437
+
438
+ print("Starting per-piece GRPO training...")
439
+ print(f"Config: {GAMES_PER_ITER} games/iter, max {MAX_STEPS_PER_GAME} steps, "
440
+ f"max {MAX_ACTIONS_PER_PIECE} tokens/piece, {NUM_ITERATIONS} iterations\n")
441
+
442
+ for iteration in range(NUM_ITERATIONS):
443
+ stats = train_one_iteration(model, optimizer, seed=iteration)
444
+ history.append(stats)
445
+
446
+ if iteration % 5 == 0 or iteration == NUM_ITERATIONS - 1:
447
+ print(f"[Iter {iteration:3d}] "
448
+ f"reward={stats['mean_reward']:+8.1f} "
449
+ f"std={stats['std_reward']:6.1f} "
450
+ f"loss={stats['loss']:7.3f} "
451
+ f"steps={stats['avg_steps']:5.1f} "
452
+ f"lines={stats['avg_lines']:4.1f} "
453
+ f"pieces={stats['avg_pieces']:4.1f}")
454
+
455
+ print("\nTraining complete!")
456
+ ```
457
+
458
+ **Step 2: Run cell, verify reward logs appear**
459
+
460
+ **Step 3: Commit**
461
+
462
+ ---
463
+
464
+ ### Task 7: Cell 7 — Plot Reward Curve
465
+
466
+ **Files:**
467
+ - Modify: `tetris_training.ipynb` Cell 7
468
+
469
+ **Step 1: Write cell content**
470
+
471
+ ```python
472
+ # Cell 7: Plot reward curve
473
+ import matplotlib.pyplot as plt
474
+
475
+ fig, axes = plt.subplots(1, 3, figsize=(18, 5))
476
+
477
+ iters = range(len(history))
478
+
479
+ axes[0].plot(iters, [h['mean_reward'] for h in history])
480
+ axes[0].set_title('Mean Reward per Iteration')
481
+ axes[0].set_xlabel('Iteration')
482
+ axes[0].set_ylabel('Reward')
483
+
484
+ axes[1].plot(iters, [h['avg_lines'] for h in history])
485
+ axes[1].set_title('Avg Lines Cleared')
486
+ axes[1].set_xlabel('Iteration')
487
+
488
+ axes[2].plot(iters, [h['loss'] for h in history])
489
+ axes[2].set_title('Policy Gradient Loss')
490
+ axes[2].set_xlabel('Iteration')
491
+
492
+ plt.tight_layout()
493
+ plt.savefig('reward_curve.png', dpi=150)
494
+ plt.show()
495
+ ```
496
+
497
+ **Step 2: Run, verify plots**
498
+
499
+ **Step 3: Commit**
500
+
501
+ ---
502
+
503
+ ### Task 8: Cell 8 — Demo Trained Model
504
+
505
+ **Files:**
506
+ - Modify: `tetris_training.ipynb` Cell 8
507
+
508
+ **Step 1: Write cell content**
509
+
510
+ ```python
511
+ # Cell 8: Demo — TRAINED model plays 3 games
512
+ print("=== TRAINED MODEL ===\n")
513
+
514
+ trained_rewards = []
515
+ for seed in [42, 123, 7]:
516
+ game = play_one_game(model, tokenizer, seed=seed)
517
+ print(f"Seed {seed}: reward={game['reward']:+.1f}, "
518
+ f"steps={game['total_steps']}, lines={game['total_lines']}, "
519
+ f"pieces={game['pieces_placed']}")
520
+ trained_rewards.append(game['reward'])
521
+
522
+ avg_trained = sum(trained_rewards) / len(trained_rewards)
523
+ print(f"\n{'='*50}")
524
+ print(f"UNTRAINED reward (seed=42): {untrained_reward:+.1f}")
525
+ print(f"TRAINED avg reward (3 games): {avg_trained:+.1f}")
526
+ print(f"Improvement: {avg_trained - untrained_reward:+.1f}")
527
+ print('='*50)
528
+ ```
529
+
530
+ **Step 2: Run, verify comparison**
531
+
532
+ **Step 3: Commit**
533
+
534
+ ---
535
+
536
+ ### Task 9: Cell 9 — Push to HF Hub
537
+
538
+ **Files:**
539
+ - Modify: `tetris_training.ipynb` Cell 9
540
+
541
+ **Step 1: Write cell content**
542
+
543
+ ```python
544
+ # Cell 9: Push trained model to HF Hub
545
+ model.push_to_hub("VortexedSquirrel/tetris-agent-grpo")
546
+ tokenizer.push_to_hub("VortexedSquirrel/tetris-agent-grpo")
547
+ print("Model pushed to https://huggingface.co/VortexedSquirrel/tetris-agent-grpo")
548
+ ```
549
+
550
+ **Step 2: Commit + push**
551
+
552
+ ```bash
553
+ git add tetris_training.ipynb
554
+ git commit -m "rewrite notebook: per-piece GRPO training on 3B"
555
+ git push origin main
556
+ ```
557
+
558
+ ---
559
+
560
+ ## Key Implementation Details
561
+
562
+ ### Piece lock detection
563
+ After `env.step()`, check `env.current_piece_name != current_piece_name`.
564
+ The engine calls `_spawn_next()` when a piece locks, changing the current piece.
565
+
566
+ ### Two-phase gradient computation
567
+ - **Rollout phase**: `torch.no_grad()`, KV cache for fast autoregressive generation
568
+ - **Update phase**: teacher-forced forward pass (prompt + actions in one call),
569
+ `piece_loss.backward()` after each piece to keep memory low
570
+
571
+ ### Memory-safe gradient accumulation
572
+ Each piece does its own `.backward()` adding to accumulated gradients.
573
+ Only one piece's computation graph in memory at a time.
574
+ `optimizer.step()` called once after all 8 games processed.
575
+
576
+ ### Time estimate (L4, 3B model)
577
+ - Rollout: 8 games × ~200 steps × ~0.02s/token ≈ 32s
578
+ - Update: 8 games × ~30 pieces × ~0.1s/piece ≈ 24s
579
+ - Total per iteration: ~56s
580
+ - 100 iterations: ~93 min ≈ 1.5 hours
tetris_training.ipynb CHANGED
@@ -5,11 +5,11 @@
5
  "metadata": {
6
  "id": "jYiMCaOSFG8J"
7
  },
8
- "source": "# Tetris OpenEnv — GRPO Training\n\nTrain an LLM agent to play Tetris using GRPO (Group Relative Policy Optimization).\n\n**Environment**: Tetris on HF Spaces via OpenEnv 0.2.1\n**Model**: Qwen2.5-7B-Instruct + LoRA (standard HF stack)\n**Training**: GRPO via TRL single-prompt, 100-action blind play\n**Runtime**: A100 GPU (Colab Pro)"
9
  },
10
  {
11
  "cell_type": "code",
12
- "execution_count": 1,
13
  "metadata": {
14
  "colab": {
15
  "base_uri": "https://localhost:8080/"
@@ -17,35 +17,12 @@
17
  "id": "PHNUG6nYFG8L",
18
  "outputId": "e913c80a-f0ec-4231-a35c-f01106a333a1"
19
  },
20
- "outputs": [
21
- {
22
- "output_type": "stream",
23
- "name": "stdout",
24
- "text": [
25
- "\u001b[33mWARNING: Skipping unsloth as it is not installed.\u001b[0m\u001b[33m\n",
26
- "\u001b[0m\u001b[33mWARNING: Skipping unsloth-zoo as it is not installed.\u001b[0m\u001b[33m\n",
27
- "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m528.8/528.8 kB\u001b[0m \u001b[31m10.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
28
- "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m121.9/121.9 kB\u001b[0m \u001b[31m15.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
29
- "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m633.7/633.7 kB\u001b[0m \u001b[31m33.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
30
- "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m251.7/251.7 kB\u001b[0m \u001b[31m20.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
31
- "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m201.9/201.9 kB\u001b[0m \u001b[31m23.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
32
- "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m96.4/96.4 kB\u001b[0m \u001b[31m11.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
33
- "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m152.3/152.3 kB\u001b[0m \u001b[31m17.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
34
- "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m80.2/80.2 kB\u001b[0m \u001b[31m9.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
35
- "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m331.1/331.1 kB\u001b[0m \u001b[31m34.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
36
- "\u001b[?25h"
37
- ]
38
- }
39
- ],
40
- "source": [
41
- "# Cell 1: Install dependencies (remove Unsloth, use standard HF stack)\n",
42
- "!pip uninstall unsloth unsloth-zoo -y -q\n",
43
- "!pip install peft trl openenv-core datasets accelerate -q\n"
44
- ]
45
  },
46
  {
47
  "cell_type": "code",
48
- "execution_count": 2,
49
  "metadata": {
50
  "colab": {
51
  "base_uri": "https://localhost:8080/",
@@ -166,208 +143,12 @@
166
  "id": "4CEG1_JMFG8L",
167
  "outputId": "fe327915-556f-409f-c75c-fc2f74a7e5f6"
168
  },
169
- "outputs": [
170
- {
171
- "output_type": "stream",
172
- "name": "stderr",
173
- "text": [
174
- "/usr/local/lib/python3.12/dist-packages/huggingface_hub/utils/_auth.py:94: UserWarning: \n",
175
- "The secret `HF_TOKEN` does not exist in your Colab secrets.\n",
176
- "To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session.\n",
177
- "You will be able to reuse this secret in all of your notebooks.\n",
178
- "Please note that authentication is recommended but still optional to access public models or datasets.\n",
179
- " warnings.warn(\n"
180
- ]
181
- },
182
- {
183
- "output_type": "display_data",
184
- "data": {
185
- "text/plain": [
186
- "config.json: 0%| | 0.00/663 [00:00<?, ?B/s]"
187
- ],
188
- "application/vnd.jupyter.widget-view+json": {
189
- "version_major": 2,
190
- "version_minor": 0,
191
- "model_id": "6285b80f4b53409094ba1877193efba5"
192
- }
193
- },
194
- "metadata": {}
195
- },
196
- {
197
- "output_type": "display_data",
198
- "data": {
199
- "text/plain": [
200
- "tokenizer_config.json: 0.00B [00:00, ?B/s]"
201
- ],
202
- "application/vnd.jupyter.widget-view+json": {
203
- "version_major": 2,
204
- "version_minor": 0,
205
- "model_id": "c34a5bdf556842fa9faaab6bac674e4b"
206
- }
207
- },
208
- "metadata": {}
209
- },
210
- {
211
- "output_type": "display_data",
212
- "data": {
213
- "text/plain": [
214
- "vocab.json: 0.00B [00:00, ?B/s]"
215
- ],
216
- "application/vnd.jupyter.widget-view+json": {
217
- "version_major": 2,
218
- "version_minor": 0,
219
- "model_id": "ed82c562c8824a1ebf459cdcbad610c9"
220
- }
221
- },
222
- "metadata": {}
223
- },
224
- {
225
- "output_type": "display_data",
226
- "data": {
227
- "text/plain": [
228
- "merges.txt: 0.00B [00:00, ?B/s]"
229
- ],
230
- "application/vnd.jupyter.widget-view+json": {
231
- "version_major": 2,
232
- "version_minor": 0,
233
- "model_id": "fcd3965e71074108a5e932cbb97c40bc"
234
- }
235
- },
236
- "metadata": {}
237
- },
238
- {
239
- "output_type": "display_data",
240
- "data": {
241
- "text/plain": [
242
- "tokenizer.json: 0.00B [00:00, ?B/s]"
243
- ],
244
- "application/vnd.jupyter.widget-view+json": {
245
- "version_major": 2,
246
- "version_minor": 0,
247
- "model_id": "6c8415ffb68445619f115c60e32c616b"
248
- }
249
- },
250
- "metadata": {}
251
- },
252
- {
253
- "output_type": "stream",
254
- "name": "stderr",
255
- "text": [
256
- "`torch_dtype` is deprecated! Use `dtype` instead!\n"
257
- ]
258
- },
259
- {
260
- "output_type": "display_data",
261
- "data": {
262
- "text/plain": [
263
- "model.safetensors.index.json: 0.00B [00:00, ?B/s]"
264
- ],
265
- "application/vnd.jupyter.widget-view+json": {
266
- "version_major": 2,
267
- "version_minor": 0,
268
- "model_id": "f7918c9c71a34e178a45e889e33cec93"
269
- }
270
- },
271
- "metadata": {}
272
- },
273
- {
274
- "output_type": "display_data",
275
- "data": {
276
- "text/plain": [
277
- "Downloading (incomplete total...): 0.00B [00:00, ?B/s]"
278
- ],
279
- "application/vnd.jupyter.widget-view+json": {
280
- "version_major": 2,
281
- "version_minor": 0,
282
- "model_id": "8c7e3c13b85d4013bd571019d6efff5f"
283
- }
284
- },
285
- "metadata": {}
286
- },
287
- {
288
- "output_type": "display_data",
289
- "data": {
290
- "text/plain": [
291
- "Fetching 4 files: 0%| | 0/4 [00:00<?, ?it/s]"
292
- ],
293
- "application/vnd.jupyter.widget-view+json": {
294
- "version_major": 2,
295
- "version_minor": 0,
296
- "model_id": "ff3dd07d11e04ca8b5bef7c77d95c7ff"
297
- }
298
- },
299
- "metadata": {}
300
- },
301
- {
302
- "output_type": "display_data",
303
- "data": {
304
- "text/plain": [
305
- "Loading weights: 0%| | 0/339 [00:00<?, ?it/s]"
306
- ],
307
- "application/vnd.jupyter.widget-view+json": {
308
- "version_major": 2,
309
- "version_minor": 0,
310
- "model_id": "cfe6c9c2bc5d460d846f2af15f9d7ed6"
311
- }
312
- },
313
- "metadata": {}
314
- },
315
- {
316
- "output_type": "display_data",
317
- "data": {
318
- "text/plain": [
319
- "generation_config.json: 0%| | 0.00/243 [00:00<?, ?B/s]"
320
- ],
321
- "application/vnd.jupyter.widget-view+json": {
322
- "version_major": 2,
323
- "version_minor": 0,
324
- "model_id": "318e1f3e9c4f4c5992a1507cd9de6d9a"
325
- }
326
- },
327
- "metadata": {}
328
- },
329
- {
330
- "output_type": "stream",
331
- "name": "stdout",
332
- "text": [
333
- "trainable params: 80,740,352 || all params: 7,696,356,864 || trainable%: 1.0491\n"
334
- ]
335
- }
336
- ],
337
- "source": [
338
- "# Cell 2: Load model (standard HF stack — no Unsloth bug)\n",
339
- "import torch\n",
340
- "from transformers import AutoModelForCausalLM, AutoTokenizer\n",
341
- "from peft import LoraConfig, get_peft_model\n",
342
- "\n",
343
- "model_name = \"Qwen/Qwen2.5-7B-Instruct\"\n",
344
- "\n",
345
- "tokenizer = AutoTokenizer.from_pretrained(model_name)\n",
346
- "if tokenizer.pad_token is None:\n",
347
- " tokenizer.pad_token = tokenizer.eos_token\n",
348
- "\n",
349
- "model = AutoModelForCausalLM.from_pretrained(\n",
350
- " model_name,\n",
351
- " torch_dtype=torch.bfloat16,\n",
352
- " device_map=\"auto\",\n",
353
- ")\n",
354
- "\n",
355
- "lora_config = LoraConfig(\n",
356
- " r=32,\n",
357
- " lora_alpha=32,\n",
358
- " lora_dropout=0,\n",
359
- " target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\",\n",
360
- " \"gate_proj\", \"up_proj\", \"down_proj\"],\n",
361
- " task_type=\"CAUSAL_LM\",\n",
362
- ")\n",
363
- "\n",
364
- "model = get_peft_model(model, lora_config)\n",
365
- "model.print_trainable_parameters()\n"
366
- ]
367
  },
368
  {
369
  "cell_type": "code",
370
- "execution_count": 3,
371
  "metadata": {
372
  "colab": {
373
  "base_uri": "https://localhost:8080/"
@@ -375,68 +156,12 @@
375
  "id": "D3HDc8_3FG8L",
376
  "outputId": "b28882c2-1497-4d3b-98b8-4b4cb03cbb0b"
377
  },
378
- "outputs": [
379
- {
380
- "output_type": "stream",
381
- "name": "stdout",
382
- "text": [
383
- "Connected! Piece: L\n",
384
- "Step OK. Reward: -7, Done: False\n",
385
- "Environment connection verified!\n"
386
- ]
387
- }
388
- ],
389
- "source": [
390
- "# Cell 3: Define Tetris client (standalone, no local package needed)\n",
391
- "import json\n",
392
- "import websockets.sync.client as ws_client\n",
393
- "\n",
394
- "TETRIS_URL = \"wss://vortexedsquirrel-tetris-env.hf.space/ws\"\n",
395
- "\n",
396
- "class TetrisClient:\n",
397
- " \"\"\"Lightweight Tetris env client for Colab.\"\"\"\n",
398
- " def __init__(self, url=TETRIS_URL):\n",
399
- " self.url = url\n",
400
- " self.ws = None\n",
401
- "\n",
402
- " def connect(self):\n",
403
- " self.ws = ws_client.connect(self.url, open_timeout=30)\n",
404
- " return self\n",
405
- "\n",
406
- " def _send_recv(self, msg):\n",
407
- " self.ws.send(json.dumps(msg))\n",
408
- " return json.loads(self.ws.recv(timeout=30))\n",
409
- "\n",
410
- " def reset(self, seed=None):\n",
411
- " data = {\"seed\": seed} if seed else {}\n",
412
- " resp = self._send_recv({\"type\": \"reset\", \"data\": data})\n",
413
- " d = resp[\"data\"]\n",
414
- " obs = d.get(\"observation\", d)\n",
415
- " return {\"observation\": obs, \"reward\": d.get(\"reward\", 0), \"done\": d.get(\"done\", False)}\n",
416
- "\n",
417
- " def step(self, action_str):\n",
418
- " resp = self._send_recv({\"type\": \"step\", \"data\": {\"action\": action_str, \"metadata\": {}}})\n",
419
- " d = resp[\"data\"]\n",
420
- " obs = d.get(\"observation\", d)\n",
421
- " return {\"observation\": obs, \"reward\": d.get(\"reward\", 0), \"done\": d.get(\"done\", False)}\n",
422
- "\n",
423
- " def close(self):\n",
424
- " if self.ws:\n",
425
- " self.ws.close()\n",
426
- "\n",
427
- "# Quick test\n",
428
- "client = TetrisClient().connect()\n",
429
- "r = client.reset(seed=42)\n",
430
- "print(f\"Connected! Piece: {r['observation']['current_piece']}\")\n",
431
- "r = client.step(\"drop\")\n",
432
- "print(f\"Step OK. Reward: {r['reward']}, Done: {r['done']}\")\n",
433
- "client.close()\n",
434
- "print(\"Environment connection verified!\")"
435
- ]
436
  },
437
  {
438
  "cell_type": "code",
439
- "execution_count": 4,
440
  "metadata": {
441
  "colab": {
442
  "base_uri": "https://localhost:8080/"
@@ -444,66 +169,12 @@
444
  "id": "3O3yawZBFG8M",
445
  "outputId": "7d8ed74b-4c46-4857-c1b7-9bb99848a93f"
446
  },
447
- "outputs": [
448
- {
449
- "output_type": "stream",
450
- "name": "stdout",
451
- "text": [
452
- "Generated 400 prompts\n"
453
- ]
454
- }
455
- ],
456
- "source": [
457
- "# Cell 4: Generate training prompts — single-char actions, 100 fixed\n",
458
- "import random\n",
459
- "\n",
460
- "!wget -q -O game_engine.py https://raw.githubusercontent.com/OutOfMystic/tetris-openenv/main/src/tetris_env/server/game_engine.py\n",
461
- "\n",
462
- "from game_engine import TetrisEnv\n",
463
- "\n",
464
- "# Action encoding: 1 char = 1 action\n",
465
- "ACTION_MAP = {'L': 'left', 'R': 'right', 'C': 'rotate_cw', 'W': 'rotate_ccw', 'D': 'drop', 'S': 'down'}\n",
466
- "ACTION_CHARS = list(ACTION_MAP.keys())\n",
467
- "\n",
468
- "SYSTEM_PROMPT = \"\"\"You are a Tetris AI. Output exactly 100 actions as single letters separated by spaces.\n",
469
- "L=left R=right C=rotate_cw W=rotate_ccw D=drop S=down\n",
470
- "Example: L L C D R R D L D S D L C D R D ...\n",
471
- "Strategy: fill complete rows. Multiple lines at once = bonus.\n",
472
- "Output ONLY the 100 letters, nothing else.\"\"\"\n",
473
- "\n",
474
- "def make_prompt(result):\n",
475
- " return f\"\"\"Board:\n",
476
- "{result['board']}\n",
477
- "\n",
478
- "Piece: {result['current_piece']} Next: {result['next_piece']}\n",
479
- "Score: {result['score']} Lines: {result['total_lines']} Height: {result['max_height']} Holes: {result['holes']}\n",
480
- "\n",
481
- "Your 100 actions:\"\"\"\n",
482
- "\n",
483
- "def generate_training_prompts(n_prompts=400):\n",
484
- " prompts = []\n",
485
- " for i in range(n_prompts):\n",
486
- " env = TetrisEnv(seed=i)\n",
487
- " result = env.reset(seed=i)\n",
488
- " rng = random.Random(i)\n",
489
- " moves = rng.randint(0, 4)\n",
490
- " direction = rng.choice([\"left\", \"right\"])\n",
491
- " for _ in range(moves):\n",
492
- " env.step(direction)\n",
493
- " result = env._make_result(0)\n",
494
- " prompts.append({\n",
495
- " \"prompt\": [{\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n",
496
- " {\"role\": \"user\", \"content\": make_prompt(result)}],\n",
497
- " })\n",
498
- " print(f\"Generated {len(prompts)} prompts\")\n",
499
- " return prompts\n",
500
- "\n",
501
- "train_prompts = generate_training_prompts(400)\n"
502
- ]
503
  },
504
  {
505
  "cell_type": "code",
506
- "execution_count": 5,
507
  "metadata": {
508
  "colab": {
509
  "base_uri": "https://localhost:8080/"
@@ -511,61 +182,19 @@
511
  "id": "JdcKswz5FG8M",
512
  "outputId": "b0533b78-b10a-4d09-bea1-1b18d8a58c54"
513
  },
514
- "outputs": [
515
- {
516
- "output_type": "stream",
517
- "name": "stdout",
518
- "text": [
519
- "Dataset size: 400\n",
520
- "Example prompt (user message):\n",
521
- "Board:\n",
522
- "+----------+\n",
523
- "|..........|\n",
524
- "|..........|\n",
525
- "|..........|\n",
526
- "|........@.|\n",
527
- "|........@.|\n",
528
- "|.......@@.|\n",
529
- "|..........|\n",
530
- "|..........|\n",
531
- "|..........|\n",
532
- "|..........|\n",
533
- "|..........|\n",
534
- "|..........|\n",
535
- "|..........|\n",
536
- "|..........|\n",
537
- "|..........|\n",
538
- "|..........|\n",
539
- "|..........|\n",
540
- "|..........|\n",
541
- "|..........|\n",
542
- "|..........|\n",
543
- "+----------+\n",
544
- "\n",
545
- "Piece:\n"
546
- ]
547
- }
548
- ],
549
- "source": [
550
- "# Cell 5: Create HF Dataset from prompts\n",
551
- "from datasets import Dataset\n",
552
- "\n",
553
- "dataset = Dataset.from_list(train_prompts)\n",
554
- "print(f\"Dataset size: {len(dataset)}\")\n",
555
- "print(f\"Example prompt (user message):\")\n",
556
- "print(dataset[0]['prompt'][1]['content'][:300])"
557
- ]
558
  },
559
  {
560
  "cell_type": "code",
561
- "source": "# Cell 5b: DemoUNTRAINED model plays one game (before training)\n# Let's see what the model does BEFORE any GRPO training\n\nimport torch\n\nNUM_ACTIONS = 100 # must match Cell 6 reward function\n\ndef play_one_game(current_model, current_tokenizer, seed=42, label=\"Model\"):\n \"\"\"Generate 100 actions with the model, play them on local engine, show results.\"\"\"\n # Create a fresh game\n env = TetrisEnv(seed=seed)\n result = env.reset(seed=seed)\n board_text = make_prompt(result)\n\n messages = [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": board_text},\n ]\n\n inputs = current_tokenizer.apply_chat_template(\n messages, return_tensors=\"pt\", add_generation_prompt=True\n ).to(current_model.device)\n\n print(f\"=== {label} playing Tetris (seed={seed}) ===\")\n print(f\"Starting piece: {result['current_piece']}, Next: {result['next_piece']}\")\n print(f\"Generating {NUM_ACTIONS} actions...\")\n\n with torch.no_grad():\n outputs = current_model.generate(\n inputs,\n max_new_tokens=220,\n temperature=0.7,\n do_sample=True,\n pad_token_id=current_tokenizer.pad_token_id,\n )\n\n response = current_tokenizer.decode(outputs[0][inputs.shape[-1]:], skip_special_tokens=True)\n print(f\"\\nRaw model output:\\n{response[:300]}\")\n\n # Parse actions\n chars = [ch for ch in response.strip().upper().split() if ch in ACTION_MAP]\n if len(chars) < 5:\n chars = [ch for ch in response.strip().upper() if ch in ACTION_MAP]\n\n print(f\"\\nParsed {len(chars)} valid actions out of {NUM_ACTIONS} needed\")\n\n # Pad or truncate\n if len(chars) < NUM_ACTIONS:\n chars += ['D'] * (NUM_ACTIONS - len(chars))\n chars = chars[:NUM_ACTIONS]\n\n print(f\"Action sequence: {' '.join(chars[:30])} ... {' '.join(chars[-10:])}\")\n\n # Play on engine\n env2 = TetrisEnv(seed=seed)\n env2.reset(seed=seed)\n steps_played = 0\n game_over = False\n\n for ch in chars:\n r = env2.step(ACTION_MAP[ch])\n steps_played += 1\n if r['done']:\n game_over = True\n break\n\n # Calculate training reward (same formula as reward function)\n total_lines = r['total_lines']\n holes = r['holes']\n height = r['max_height']\n reward = total_lines * 200.0 + steps_played * 0.5 - holes * 2.0 - height * 1.0\n if game_over:\n reward -= 50.0\n\n print(f\"\\n--- Result ---\")\n print(f\"Steps played: {steps_played}/{NUM_ACTIONS} {'(GAME OVER!)' if game_over else '(survived)'}\")\n print(f\"Lines cleared: {total_lines}\")\n print(f\"Score: {r['score']}\")\n print(f\"Max height: {height}\")\n print(f\"Holes: {holes}\")\n print(f\"Training reward: {reward:+.1f}\")\n print(f\"\\nFinal board:\")\n print(r['board'])\n\n return reward\n\n# Run demo with untrained model\nuntrained_reward = play_one_game(model, tokenizer, seed=42, label=\"UNTRAINED model\")\nprint(f\"\\n{'='*50}\")\nprint(f\"Untrained model reward: {untrained_reward:+.1f}\")\nprint(f\"(Remember: higher is better, positive = good play)\")",
562
  "metadata": {},
563
  "execution_count": null,
564
  "outputs": []
565
  },
566
  {
567
  "cell_type": "code",
568
- "execution_count": 6,
569
  "metadata": {
570
  "colab": {
571
  "base_uri": "https://localhost:8080/"
@@ -573,70 +202,8 @@
573
  "id": "1SRpxwy-FG8M",
574
  "outputId": "30c168c0-faf0-4c26-fd74-62d3ed3ab13b"
575
  },
576
- "outputs": [
577
- {
578
- "output_type": "stream",
579
- "name": "stdout",
580
- "text": [
581
- "Test rewards: [-157.5, -138.5, -94.0]\n"
582
- ]
583
- }
584
- ],
585
- "source": [
586
- "# Cell 6: Reward function — parse single chars, exactly 100 actions\n",
587
- "\n",
588
- "NUM_ACTIONS = 100\n",
589
- "\n",
590
- "def tetris_reward_func(completions, **kwargs):\n",
591
- " rewards = []\n",
592
- " for i, completion in enumerate(completions):\n",
593
- " text = completion[0]['content'].strip().upper() if isinstance(completion, list) else str(completion).strip().upper()\n",
594
- "\n",
595
- " # Parse single-char actions\n",
596
- " chars = [ch for ch in text.split() if ch in ACTION_MAP]\n",
597
- " # Also try without spaces (e.g. \"LLCDRRД\")\n",
598
- " if len(chars) < 5:\n",
599
- " chars = [ch for ch in text if ch in ACTION_MAP]\n",
600
- "\n",
601
- " # Pad to 100 or truncate to 100\n",
602
- " if len(chars) < NUM_ACTIONS:\n",
603
- " chars += ['D'] * (NUM_ACTIONS - len(chars)) # pad with drop\n",
604
- " chars = chars[:NUM_ACTIONS]\n",
605
- "\n",
606
- " # Play on engine\n",
607
- " env = TetrisEnv(seed=i)\n",
608
- " env.reset(seed=i)\n",
609
- "\n",
610
- " total_lines = 0\n",
611
- " steps_played = 0\n",
612
- " game_over = False\n",
613
- "\n",
614
- " for ch in chars:\n",
615
- " result = env.step(ACTION_MAP[ch])\n",
616
- " steps_played += 1\n",
617
- " total_lines = result['total_lines']\n",
618
- " if result['done']:\n",
619
- " game_over = True\n",
620
- " break\n",
621
- "\n",
622
- " reward = 0.0\n",
623
- " reward += total_lines * 200.0\n",
624
- " reward += steps_played * 0.5\n",
625
- " reward -= result['holes'] * 2.0\n",
626
- " reward -= result['max_height'] * 1.0\n",
627
- " if game_over:\n",
628
- " reward -= 50.0\n",
629
- "\n",
630
- " rewards.append(reward)\n",
631
- "\n",
632
- " return rewards\n",
633
- "\n",
634
- "# Test\n",
635
- "test = [[{\"content\": \"L L C D R R D L D S D \" * 6}],\n",
636
- " [{\"content\": \"D D D D D D D D D D \" * 6}],\n",
637
- " [{\"content\": \"garbage text no actions\"}]]\n",
638
- "print(f\"Test rewards: {tetris_reward_func(test)}\")\n"
639
- ]
640
  },
641
  {
642
  "cell_type": "code",
@@ -649,350 +216,15 @@
649
  "id": "UKvvm6gPFG8M",
650
  "outputId": "413432d5-f511-40d5-eb43-0874012e593d"
651
  },
652
- "outputs": [
653
- {
654
- "output_type": "stream",
655
- "name": "stderr",
656
- "text": [
657
- "The tokenizer has new PAD/BOS/EOS tokens that differ from the model config and generation config. The model config and generation config were aligned accordingly, being updated with the tokenizer's values. Updated tokens: {'bos_token_id': None, 'pad_token_id': 151643}.\n"
658
- ]
659
- },
660
- {
661
- "output_type": "stream",
662
- "name": "stdout",
663
- "text": [
664
- "Starting GRPO training...\n"
665
- ]
666
- },
667
- {
668
- "output_type": "stream",
669
- "name": "stderr",
670
- "text": [
671
- "Passing `generation_config` together with generation-related arguments=({'disable_compile'}) is deprecated and will be removed in future versions. Please pass either a `generation_config` object OR all generation parameters explicitly, but not both.\n"
672
- ]
673
- },
674
- {
675
- "output_type": "display_data",
676
- "data": {
677
- "text/plain": [
678
- "<IPython.core.display.HTML object>"
679
- ],
680
- "text/html": [
681
- "\n",
682
- " <div>\n",
683
- " \n",
684
- " <progress value='404' max='1200' style='width:300px; height:20px; vertical-align: middle;'></progress>\n",
685
- " [ 404/1200 1:56:25 < 3:50:31, 0.06 it/s, Epoch 1.01/3]\n",
686
- " </div>\n",
687
- " <table border=\"1\" class=\"dataframe\">\n",
688
- " <thead>\n",
689
- " <tr style=\"text-align: left;\">\n",
690
- " <th>Step</th>\n",
691
- " <th>Training Loss</th>\n",
692
- " </tr>\n",
693
- " </thead>\n",
694
- " <tbody>\n",
695
- " <tr>\n",
696
- " <td>10</td>\n",
697
- " <td>-0.034254</td>\n",
698
- " </tr>\n",
699
- " <tr>\n",
700
- " <td>20</td>\n",
701
- " <td>-0.039996</td>\n",
702
- " </tr>\n",
703
- " <tr>\n",
704
- " <td>30</td>\n",
705
- " <td>-0.005777</td>\n",
706
- " </tr>\n",
707
- " <tr>\n",
708
- " <td>40</td>\n",
709
- " <td>-0.003115</td>\n",
710
- " </tr>\n",
711
- " <tr>\n",
712
- " <td>50</td>\n",
713
- " <td>-0.006602</td>\n",
714
- " </tr>\n",
715
- " <tr>\n",
716
- " <td>60</td>\n",
717
- " <td>-0.034982</td>\n",
718
- " </tr>\n",
719
- " <tr>\n",
720
- " <td>70</td>\n",
721
- " <td>-0.017585</td>\n",
722
- " </tr>\n",
723
- " <tr>\n",
724
- " <td>80</td>\n",
725
- " <td>-0.069048</td>\n",
726
- " </tr>\n",
727
- " <tr>\n",
728
- " <td>90</td>\n",
729
- " <td>-0.050287</td>\n",
730
- " </tr>\n",
731
- " <tr>\n",
732
- " <td>100</td>\n",
733
- " <td>0.014232</td>\n",
734
- " </tr>\n",
735
- " <tr>\n",
736
- " <td>110</td>\n",
737
- " <td>0.000296</td>\n",
738
- " </tr>\n",
739
- " <tr>\n",
740
- " <td>120</td>\n",
741
- " <td>-0.000809</td>\n",
742
- " </tr>\n",
743
- " <tr>\n",
744
- " <td>130</td>\n",
745
- " <td>0.006139</td>\n",
746
- " </tr>\n",
747
- " <tr>\n",
748
- " <td>140</td>\n",
749
- " <td>0.002477</td>\n",
750
- " </tr>\n",
751
- " <tr>\n",
752
- " <td>150</td>\n",
753
- " <td>-0.012867</td>\n",
754
- " </tr>\n",
755
- " <tr>\n",
756
- " <td>160</td>\n",
757
- " <td>0.003067</td>\n",
758
- " </tr>\n",
759
- " <tr>\n",
760
- " <td>170</td>\n",
761
- " <td>-0.006496</td>\n",
762
- " </tr>\n",
763
- " <tr>\n",
764
- " <td>180</td>\n",
765
- " <td>0.006063</td>\n",
766
- " </tr>\n",
767
- " <tr>\n",
768
- " <td>190</td>\n",
769
- " <td>0.003406</td>\n",
770
- " </tr>\n",
771
- " <tr>\n",
772
- " <td>200</td>\n",
773
- " <td>-0.022040</td>\n",
774
- " </tr>\n",
775
- " <tr>\n",
776
- " <td>210</td>\n",
777
- " <td>-0.025758</td>\n",
778
- " </tr>\n",
779
- " <tr>\n",
780
- " <td>220</td>\n",
781
- " <td>0.000000</td>\n",
782
- " </tr>\n",
783
- " <tr>\n",
784
- " <td>230</td>\n",
785
- " <td>-0.012708</td>\n",
786
- " </tr>\n",
787
- " <tr>\n",
788
- " <td>240</td>\n",
789
- " <td>-0.000000</td>\n",
790
- " </tr>\n",
791
- " <tr>\n",
792
- " <td>250</td>\n",
793
- " <td>0.001564</td>\n",
794
- " </tr>\n",
795
- " <tr>\n",
796
- " <td>260</td>\n",
797
- " <td>-0.000000</td>\n",
798
- " </tr>\n",
799
- " <tr>\n",
800
- " <td>270</td>\n",
801
- " <td>0.000000</td>\n",
802
- " </tr>\n",
803
- " <tr>\n",
804
- " <td>280</td>\n",
805
- " <td>0.000000</td>\n",
806
- " </tr>\n",
807
- " <tr>\n",
808
- " <td>290</td>\n",
809
- " <td>0.000000</td>\n",
810
- " </tr>\n",
811
- " <tr>\n",
812
- " <td>300</td>\n",
813
- " <td>-0.000000</td>\n",
814
- " </tr>\n",
815
- " <tr>\n",
816
- " <td>310</td>\n",
817
- " <td>-0.000000</td>\n",
818
- " </tr>\n",
819
- " <tr>\n",
820
- " <td>320</td>\n",
821
- " <td>0.000000</td>\n",
822
- " </tr>\n",
823
- " <tr>\n",
824
- " <td>330</td>\n",
825
- " <td>-0.001915</td>\n",
826
- " </tr>\n",
827
- " <tr>\n",
828
- " <td>340</td>\n",
829
- " <td>-0.001833</td>\n",
830
- " </tr>\n",
831
- " <tr>\n",
832
- " <td>350</td>\n",
833
- " <td>0.000000</td>\n",
834
- " </tr>\n",
835
- " <tr>\n",
836
- " <td>360</td>\n",
837
- " <td>0.004288</td>\n",
838
- " </tr>\n",
839
- " <tr>\n",
840
- " <td>370</td>\n",
841
- " <td>-0.000000</td>\n",
842
- " </tr>\n",
843
- " <tr>\n",
844
- " <td>380</td>\n",
845
- " <td>0.003926</td>\n",
846
- " </tr>\n",
847
- " <tr>\n",
848
- " <td>390</td>\n",
849
- " <td>0.000000</td>\n",
850
- " </tr>\n",
851
- " <tr>\n",
852
- " <td>400</td>\n",
853
- " <td>0.000000</td>\n",
854
- " </tr>\n",
855
- " </tbody>\n",
856
- "</table><p>"
857
- ]
858
- },
859
- "metadata": {}
860
- },
861
- {
862
- "output_type": "stream",
863
- "name": "stdout",
864
- "text": [
865
- "[Step 10] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.725, completions/max_length=220.000, completions/max_terminated_length=150.300, completions/mean_length=192.387, completions/mean_terminated_length=118.600, completions/min_length=88.300, completions/min_terminated_length=88.300, entropy=0.050, frac_reward_zero_std=0.000, grad_norm=0.105, num_tokens=31799.000, reward=-92.294, reward_std=55.018, rewards/tetris_reward_func/mean=-92.294, rewards/tetris_reward_func/std=55.018, step_time=17.325\n",
866
- "[Step 20] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.762, completions/max_length=220.000, completions/max_terminated_length=109.700, completions/mean_length=194.350, completions/mean_terminated_length=90.833, completions/min_length=115.200, completions/min_terminated_length=71.200, entropy=0.049, frac_reward_zero_std=0.000, grad_norm=0.154, num_tokens=63787.000, reward=-87.744, reward_std=64.287, rewards/tetris_reward_func/mean=-87.744, rewards/tetris_reward_func/std=64.287, step_time=17.077\n",
867
- "[Step 30] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.688, completions/max_length=220.000, completions/max_terminated_length=132.100, completions/mean_length=185.338, completions/mean_terminated_length=113.600, completions/min_length=96.500, completions/min_terminated_length=96.500, entropy=0.051, frac_reward_zero_std=0.000, grad_norm=0.127, num_tokens=94966.000, reward=-84.419, reward_std=60.849, rewards/tetris_reward_func/mean=-84.419, rewards/tetris_reward_func/std=60.849, step_time=17.085\n",
868
- "[Step 40] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.725, completions/max_length=220.000, completions/max_terminated_length=114.200, completions/mean_length=189.113, completions/mean_terminated_length=100.702, completions/min_length=107.200, completions/min_terminated_length=85.200, entropy=0.049, frac_reward_zero_std=0.000, grad_norm=0.059, num_tokens=126439.000, reward=-82.194, reward_std=65.590, rewards/tetris_reward_func/mean=-82.194, rewards/tetris_reward_func/std=65.590, step_time=17.078\n",
869
- "[Step 50] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.738, completions/max_length=220.000, completions/max_terminated_length=128.500, completions/mean_length=193.863, completions/mean_terminated_length=111.117, completions/min_length=120.200, completions/min_terminated_length=98.200, entropy=0.041, frac_reward_zero_std=0.000, grad_norm=0.061, num_tokens=158364.000, reward=-72.206, reward_std=68.607, rewards/tetris_reward_func/mean=-72.206, rewards/tetris_reward_func/std=68.607, step_time=17.067\n",
870
- "[Step 60] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.800, completions/max_length=220.000, completions/max_terminated_length=133.100, completions/mean_length=200.500, completions/mean_terminated_length=109.633, completions/min_length=111.000, completions/min_terminated_length=89.000, entropy=0.038, frac_reward_zero_std=0.000, grad_norm=0.059, num_tokens=190708.000, reward=-66.744, reward_std=67.078, rewards/tetris_reward_func/mean=-66.744, rewards/tetris_reward_func/std=67.078, step_time=17.116\n",
871
- "[Step 70] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.800, completions/max_length=220.000, completions/max_terminated_length=121.900, completions/mean_length=200.662, completions/mean_terminated_length=111.100, completions/min_length=119.600, completions/min_terminated_length=97.600, entropy=0.039, frac_reward_zero_std=0.000, grad_norm=0.107, num_tokens=223145.000, reward=-46.081, reward_std=68.570, rewards/tetris_reward_func/mean=-46.081, rewards/tetris_reward_func/std=68.570, step_time=17.263\n",
872
- "[Step 80] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.738, completions/max_length=220.000, completions/max_terminated_length=127.900, completions/mean_length=193.125, completions/mean_terminated_length=111.063, completions/min_length=96.800, completions/min_terminated_length=96.800, entropy=0.052, frac_reward_zero_std=0.000, grad_norm=0.153, num_tokens=254939.000, reward=-19.050, reward_std=55.485, rewards/tetris_reward_func/mean=-19.050, rewards/tetris_reward_func/std=55.485, step_time=17.245\n",
873
- "[Step 90] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.750, completions/max_length=220.000, completions/max_terminated_length=119.900, completions/mean_length=197.775, completions/mean_terminated_length=103.608, completions/min_length=128.900, completions/min_terminated_length=84.900, entropy=0.052, frac_reward_zero_std=0.000, grad_norm=0.117, num_tokens=287121.000, reward=6.112, reward_std=42.999, rewards/tetris_reward_func/mean=6.112, rewards/tetris_reward_func/std=42.999, step_time=17.200\n",
874
- "[Step 100] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.900, completions/max_length=220.000, completions/max_terminated_length=61.200, completions/mean_length=209.688, completions/mean_terminated_length=59.300, completions/min_length=167.400, completions/min_terminated_length=57.400, entropy=0.042, frac_reward_zero_std=0.000, grad_norm=0.099, num_tokens=320240.000, reward=6.544, reward_std=43.744, rewards/tetris_reward_func/mean=6.544, rewards/tetris_reward_func/std=43.744, step_time=17.200\n",
875
- "[Step 110] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.988, completions/max_length=220.000, completions/max_terminated_length=16.400, completions/mean_length=219.300, completions/mean_terminated_length=16.400, completions/min_length=214.400, completions/min_terminated_length=16.400, entropy=0.035, frac_reward_zero_std=0.000, grad_norm=0.117, num_tokens=354208.000, reward=14.625, reward_std=26.601, rewards/tetris_reward_func/mean=14.625, rewards/tetris_reward_func/std=26.601, step_time=17.167\n",
876
- "[Step 120] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.938, completions/max_length=220.000, completions/max_terminated_length=60.600, completions/mean_length=215.675, completions/mean_terminated_length=57.950, completions/min_length=187.300, completions/min_terminated_length=55.300, entropy=0.035, frac_reward_zero_std=0.000, grad_norm=0.119, num_tokens=387822.000, reward=16.738, reward_std=15.117, rewards/tetris_reward_func/mean=16.738, rewards/tetris_reward_func/std=15.117, step_time=17.196\n",
877
- "[Step 130] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.950, completions/max_length=220.000, completions/max_terminated_length=61.300, completions/mean_length=216.662, completions/mean_terminated_length=61.300, completions/min_length=193.300, completions/min_terminated_length=61.300, entropy=0.035, frac_reward_zero_std=0.000, grad_norm=0.103, num_tokens=421579.000, reward=16.794, reward_std=25.586, rewards/tetris_reward_func/mean=16.794, rewards/tetris_reward_func/std=25.586, step_time=17.231\n",
878
- "[Step 140] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.925, completions/max_length=220.000, completions/max_terminated_length=82.500, completions/mean_length=215.450, completions/mean_terminated_length=80.400, completions/min_length=188.300, completions/min_terminated_length=78.300, entropy=0.041, frac_reward_zero_std=0.000, grad_norm=0.142, num_tokens=455215.000, reward=24.444, reward_std=27.435, rewards/tetris_reward_func/mean=24.444, rewards/tetris_reward_func/std=27.435, step_time=17.233\n"
879
- ]
880
- },
881
- {
882
- "output_type": "stream",
883
- "name": "stderr",
884
- "text": [
885
- "Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.\n",
886
- "WARNING:huggingface_hub.utils._http:Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.\n"
887
- ]
888
- },
889
- {
890
- "output_type": "stream",
891
- "name": "stdout",
892
- "text": [
893
- "[Step 150] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.963, completions/max_length=220.000, completions/max_terminated_length=23.100, completions/mean_length=215.700, completions/mean_terminated_length=20.800, completions/min_length=194.500, completions/min_terminated_length=18.500, entropy=0.053, frac_reward_zero_std=0.000, grad_norm=0.098, num_tokens=488775.000, reward=26.969, reward_std=42.222, rewards/tetris_reward_func/mean=26.969, rewards/tetris_reward_func/std=42.222, step_time=17.195\n",
894
- "[Step 160] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.938, completions/max_length=220.000, completions/max_terminated_length=70.000, completions/mean_length=215.000, completions/mean_terminated_length=70.000, completions/min_length=180.000, completions/min_terminated_length=70.000, entropy=0.061, frac_reward_zero_std=0.000, grad_norm=0.141, num_tokens=522343.000, reward=23.725, reward_std=18.791, rewards/tetris_reward_func/mean=23.725, rewards/tetris_reward_func/std=18.791, step_time=17.401\n",
895
- "[Step 170] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.988, completions/max_length=220.000, completions/max_terminated_length=13.200, completions/mean_length=218.900, completions/mean_terminated_length=13.200, completions/min_length=211.200, completions/min_terminated_length=13.200, entropy=0.083, frac_reward_zero_std=0.000, grad_norm=0.217, num_tokens=556311.000, reward=24.812, reward_std=26.831, rewards/tetris_reward_func/mean=24.812, rewards/tetris_reward_func/std=26.831, step_time=17.467\n",
896
- "[Step 180] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.988, completions/max_length=220.000, completions/max_terminated_length=14.300, completions/mean_length=219.037, completions/mean_terminated_length=14.300, completions/min_length=212.300, completions/min_terminated_length=14.300, entropy=0.093, frac_reward_zero_std=0.000, grad_norm=0.196, num_tokens=590178.000, reward=27.462, reward_std=19.135, rewards/tetris_reward_func/mean=27.462, rewards/tetris_reward_func/std=19.135, step_time=17.405\n",
897
- "[Step 190] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.975, completions/max_length=220.000, completions/max_terminated_length=29.900, completions/mean_length=218.238, completions/mean_terminated_length=29.900, completions/min_length=205.900, completions/min_terminated_length=29.900, entropy=0.119, frac_reward_zero_std=0.000, grad_norm=0.220, num_tokens=623981.000, reward=13.000, reward_std=26.462, rewards/tetris_reward_func/mean=13.000, rewards/tetris_reward_func/std=26.462, step_time=17.372\n",
898
- "[Step 200] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.938, completions/max_length=220.000, completions/max_terminated_length=49.800, completions/mean_length=213.275, completions/mean_terminated_length=47.650, completions/min_length=177.500, completions/min_terminated_length=45.500, entropy=0.186, frac_reward_zero_std=0.000, grad_norm=0.273, num_tokens=657443.000, reward=13.613, reward_std=22.587, rewards/tetris_reward_func/mean=13.613, rewards/tetris_reward_func/std=22.587, step_time=17.294\n",
899
- "[Step 210] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.863, completions/max_length=220.000, completions/max_terminated_length=98.300, completions/mean_length=208.775, completions/mean_terminated_length=92.817, completions/min_length=154.200, completions/min_terminated_length=88.200, entropy=0.255, frac_reward_zero_std=0.000, grad_norm=0.229, num_tokens=690505.000, reward=18.550, reward_std=19.927, rewards/tetris_reward_func/mean=18.550, rewards/tetris_reward_func/std=19.927, step_time=17.190\n",
900
- "[Step 220] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.281, frac_reward_zero_std=0.000, grad_norm=0.288, num_tokens=724465.000, reward=19.150, reward_std=20.560, rewards/tetris_reward_func/mean=19.150, rewards/tetris_reward_func/std=20.560, step_time=17.162\n",
901
- "[Step 230] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.975, completions/max_length=220.000, completions/max_terminated_length=25.300, completions/mean_length=217.662, completions/mean_terminated_length=25.300, completions/min_length=201.300, completions/min_terminated_length=25.300, entropy=0.201, frac_reward_zero_std=0.000, grad_norm=0.329, num_tokens=758206.000, reward=18.900, reward_std=18.913, rewards/tetris_reward_func/mean=18.900, rewards/tetris_reward_func/std=18.913, step_time=17.190\n",
902
- "[Step 240] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.198, frac_reward_zero_std=0.000, grad_norm=0.214, num_tokens=792254.000, reward=19.625, reward_std=16.034, rewards/tetris_reward_func/mean=19.625, rewards/tetris_reward_func/std=16.034, step_time=17.204\n",
903
- "[Step 250] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.975, completions/max_length=220.000, completions/max_terminated_length=35.000, completions/mean_length=218.875, completions/mean_terminated_length=35.000, completions/min_length=211.000, completions/min_terminated_length=35.000, entropy=0.192, frac_reward_zero_std=0.000, grad_norm=0.198, num_tokens=826172.000, reward=25.400, reward_std=11.692, rewards/tetris_reward_func/mean=25.400, rewards/tetris_reward_func/std=11.692, step_time=17.102\n",
904
- "[Step 260] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.173, frac_reward_zero_std=0.000, grad_norm=0.223, num_tokens=860124.000, reward=25.200, reward_std=15.597, rewards/tetris_reward_func/mean=25.200, rewards/tetris_reward_func/std=15.597, step_time=17.231\n",
905
- "[Step 270] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.221, frac_reward_zero_std=0.000, grad_norm=0.288, num_tokens=894140.000, reward=25.312, reward_std=12.701, rewards/tetris_reward_func/mean=25.312, rewards/tetris_reward_func/std=12.701, step_time=17.220\n",
906
- "[Step 280] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.243, frac_reward_zero_std=0.000, grad_norm=0.274, num_tokens=928092.000, reward=26.238, reward_std=12.358, rewards/tetris_reward_func/mean=26.238, rewards/tetris_reward_func/std=12.358, step_time=17.183\n",
907
- "[Step 290] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.151, frac_reward_zero_std=0.000, grad_norm=0.191, num_tokens=962060.000, reward=26.725, reward_std=13.212, rewards/tetris_reward_func/mean=26.725, rewards/tetris_reward_func/std=13.212, step_time=17.353\n",
908
- "[Step 300] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.135, frac_reward_zero_std=0.000, grad_norm=0.218, num_tokens=996084.000, reward=27.775, reward_std=11.482, rewards/tetris_reward_func/mean=27.775, rewards/tetris_reward_func/std=11.482, step_time=17.287\n",
909
- "[Step 310] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.134, frac_reward_zero_std=0.000, grad_norm=0.223, num_tokens=1030036.000, reward=25.488, reward_std=12.503, rewards/tetris_reward_func/mean=25.488, rewards/tetris_reward_func/std=12.503, step_time=17.345\n",
910
- "[Step 320] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.148, frac_reward_zero_std=0.000, grad_norm=0.251, num_tokens=1064028.000, reward=23.150, reward_std=15.532, rewards/tetris_reward_func/mean=23.150, rewards/tetris_reward_func/std=15.532, step_time=17.364\n",
911
- "[Step 330] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.988, completions/max_length=220.000, completions/max_terminated_length=18.400, completions/mean_length=219.550, completions/mean_terminated_length=18.400, completions/min_length=216.400, completions/min_terminated_length=18.400, entropy=0.179, frac_reward_zero_std=0.000, grad_norm=0.244, num_tokens=1097984.000, reward=24.875, reward_std=14.287, rewards/tetris_reward_func/mean=24.875, rewards/tetris_reward_func/std=14.287, step_time=17.403\n",
912
- "[Step 340] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.963, completions/max_length=220.000, completions/max_terminated_length=40.900, completions/mean_length=219.150, completions/mean_terminated_length=40.350, completions/min_length=215.800, completions/min_terminated_length=39.800, entropy=0.239, frac_reward_zero_std=0.000, grad_norm=0.319, num_tokens=1131876.000, reward=26.175, reward_std=12.656, rewards/tetris_reward_func/mean=26.175, rewards/tetris_reward_func/std=12.656, step_time=17.309\n",
913
- "[Step 350] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.257, frac_reward_zero_std=0.000, grad_norm=0.404, num_tokens=1165884.000, reward=24.625, reward_std=12.939, rewards/tetris_reward_func/mean=24.625, rewards/tetris_reward_func/std=12.939, step_time=17.213\n",
914
- "[Step 360] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.975, completions/max_length=220.000, completions/max_terminated_length=31.200, completions/mean_length=218.400, completions/mean_terminated_length=31.200, completions/min_length=207.200, completions/min_terminated_length=31.200, entropy=0.283, frac_reward_zero_std=0.000, grad_norm=0.297, num_tokens=1199772.000, reward=26.212, reward_std=13.559, rewards/tetris_reward_func/mean=26.212, rewards/tetris_reward_func/std=13.559, step_time=17.201\n",
915
- "[Step 370] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.232, frac_reward_zero_std=0.000, grad_norm=0.318, num_tokens=1233708.000, reward=28.762, reward_std=9.307, rewards/tetris_reward_func/mean=28.762, rewards/tetris_reward_func/std=9.307, step_time=17.198\n",
916
- "[Step 380] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=0.988, completions/max_length=220.000, completions/max_terminated_length=16.400, completions/mean_length=219.300, completions/mean_terminated_length=16.400, completions/min_length=214.400, completions/min_terminated_length=16.400, entropy=0.198, frac_reward_zero_std=0.000, grad_norm=0.325, num_tokens=1267652.000, reward=31.312, reward_std=9.031, rewards/tetris_reward_func/mean=31.312, rewards/tetris_reward_func/std=9.031, step_time=17.213\n",
917
- "[Step 390] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.141, frac_reward_zero_std=0.000, grad_norm=0.231, num_tokens=1301596.000, reward=28.175, reward_std=10.578, rewards/tetris_reward_func/mean=28.175, rewards/tetris_reward_func/std=10.578, step_time=17.176\n",
918
- "[Step 400] clip_ratio/high_max=0.000, clip_ratio/high_mean=0.000, clip_ratio/low_mean=0.000, clip_ratio/low_min=0.000, clip_ratio/region_mean=0.000, completions/clipped_ratio=1.000, completions/max_length=220.000, completions/max_terminated_length=0.000, completions/mean_length=220.000, completions/mean_terminated_length=0.000, completions/min_length=220.000, completions/min_terminated_length=0.000, entropy=0.122, frac_reward_zero_std=0.000, grad_norm=0.358, num_tokens=1335572.000, reward=28.887, reward_std=10.549, rewards/tetris_reward_func/mean=28.887, rewards/tetris_reward_func/std=10.549, step_time=17.246\n"
919
- ]
920
- }
921
- ],
922
- "source": [
923
- "# Cell 7: Configure and run GRPO training (A100, with reward logging)\n",
924
- "from trl import GRPOConfig, GRPOTrainer\n",
925
- "from transformers import TrainerCallback\n",
926
- "\n",
927
- "class RewardLogger(TrainerCallback):\n",
928
- " def on_log(self, args, state, control, logs=None, **kwargs):\n",
929
- " if not logs:\n",
930
- " return\n",
931
- " step = state.global_step\n",
932
- " parts = []\n",
933
- " for k in sorted(logs):\n",
934
- " if k != 'loss' and k != 'learning_rate' and k != 'epoch':\n",
935
- " parts.append(f\"{k}={logs[k]:.3f}\")\n",
936
- " if parts:\n",
937
- " print(f\"[Step {step}] {', '.join(parts)}\")\n",
938
- "\n",
939
- "training_args = GRPOConfig(\n",
940
- " output_dir=\"tetris-agent-grpo\",\n",
941
- " num_train_epochs=3,\n",
942
- " per_device_train_batch_size=4,\n",
943
- " gradient_accumulation_steps=2,\n",
944
- " num_generations=8,\n",
945
- " max_completion_length=220,\n",
946
- " learning_rate=5e-6,\n",
947
- " logging_steps=10, # log every 10 steps (less spam)\n",
948
- " save_steps=50,\n",
949
- " bf16=True,\n",
950
- " report_to=\"none\",\n",
951
- ")\n",
952
- "\n",
953
- "trainer = GRPOTrainer(\n",
954
- " model=model,\n",
955
- " processing_class=tokenizer,\n",
956
- " reward_funcs=[tetris_reward_func],\n",
957
- " args=training_args,\n",
958
- " train_dataset=dataset,\n",
959
- " callbacks=[RewardLogger()],\n",
960
- ")\n",
961
- "\n",
962
- "print(\"Starting GRPO training...\")\n",
963
- "trainer.train()\n",
964
- "print(\"Training complete!\")\n"
965
- ]
966
  },
967
  {
968
  "cell_type": "code",
969
- "source": "# Cell 8: Plot reward curve\nimport matplotlib.pyplot as plt\n\nlogs = trainer.state.log_history\ntrain_logs = [l for l in logs if 'loss' in l]\nreward_logs = [l for l in logs if 'reward' in l or 'rewards/mean' in l]\n\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\n\nif train_logs:\n axes[0].plot([l.get('step', i) for i, l in enumerate(train_logs)],\n [l['loss'] for l in train_logs])\n axes[0].set_title('Training Loss')\n axes[0].set_xlabel('Step')\n axes[0].set_ylabel('Loss')\n\nreward_key = 'reward' if reward_logs and 'reward' in reward_logs[0] else 'rewards/mean'\nif reward_logs:\n axes[1].plot([l.get('step', i) for i, l in enumerate(reward_logs)],\n [l.get(reward_key, 0) for l in reward_logs])\n axes[1].set_title('Mean Reward (should go up!)')\n axes[1].set_xlabel('Step')\n axes[1].set_ylabel('Reward')\n\nplt.tight_layout()\nplt.savefig('reward_curve.png', dpi=150)\nplt.show()\nprint(\"Reward curve saved to reward_curve.png\")",
970
  "metadata": {},
971
  "execution_count": null,
972
  "outputs": []
973
- },
974
- {
975
- "cell_type": "code",
976
- "execution_count": null,
977
- "metadata": {
978
- "id": "HUXSqr3nFG8N"
979
- },
980
- "outputs": [],
981
- "source": "# Cell 9: Demo — TRAINED model plays Tetris (after training)\n# Compare with untrained model from Cell 5b!\n\nprint(\"Playing 3 games with TRAINED model and comparing to untrained baseline...\\n\")\n\ntrained_rewards = []\nfor seed in [42, 123, 7]:\n r = play_one_game(model, tokenizer, seed=seed, label=f\"TRAINED model\")\n trained_rewards.append(r)\n print()\n\navg_trained = sum(trained_rewards) / len(trained_rewards)\n\nprint(\"=\" * 60)\nprint(f\"UNTRAINED model reward (seed=42): {untrained_reward:+.1f}\")\nprint(f\"TRAINED model avg reward (3 games): {avg_trained:+.1f}\")\nprint(f\"Improvement: {avg_trained - untrained_reward:+.1f}\")\nprint(\"=\" * 60)\n\nif avg_trained > untrained_reward:\n print(\"Training improved the model! GRPO is working.\")\nelse:\n print(\"Model needs more training epochs or tuning.\")\n"
982
- },
983
- {
984
- "cell_type": "code",
985
- "execution_count": null,
986
- "metadata": {
987
- "id": "BnOW5pcKFG8N"
988
- },
989
- "outputs": [],
990
- "source": [
991
- "# Cell 10: Push trained model to HF Hub\n",
992
- "model.push_to_hub(\"VortexedSquirrel/tetris-agent-grpo\")\n",
993
- "tokenizer.push_to_hub(\"VortexedSquirrel/tetris-agent-grpo\")\n",
994
- "print(\"Model pushed to https://huggingface.co/VortexedSquirrel/tetris-agent-grpo\")"
995
- ]
996
  }
997
  ],
998
  "metadata": {
 
5
  "metadata": {
6
  "id": "jYiMCaOSFG8J"
7
  },
8
+ "source": "# Tetris OpenEnv — Per-Piece GRPO Training\n\nTrain an LLM to play Tetris: model sees the board before **every piece**, outputs actions until piece locks, learns via GRPO-style policy gradient.\n\n**Environment**: Local Tetris engine (game_engine.py from OpenEnv)\n**Model**: Qwen2.5-3B-Instruct + LoRA (r=16)\n**Training**: Custom REINFORCE/GRPO loop8 games/iteration, 100 iterations\n**Runtime**: L4 GPU (Colab)"
9
  },
10
  {
11
  "cell_type": "code",
12
+ "execution_count": null,
13
  "metadata": {
14
  "colab": {
15
  "base_uri": "https://localhost:8080/"
 
17
  "id": "PHNUG6nYFG8L",
18
  "outputId": "e913c80a-f0ec-4231-a35c-f01106a333a1"
19
  },
20
+ "outputs": [],
21
+ "source": "# Cell 1: Install dependencies\n!pip install peft accelerate -q"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  },
23
  {
24
  "cell_type": "code",
25
+ "execution_count": null,
26
  "metadata": {
27
  "colab": {
28
  "base_uri": "https://localhost:8080/",
 
143
  "id": "4CEG1_JMFG8L",
144
  "outputId": "fe327915-556f-409f-c75c-fc2f74a7e5f6"
145
  },
146
+ "outputs": [],
147
+ "source": "# Cell 2: Load Qwen2.5-3B-Instruct + LoRA\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\nfrom peft import LoraConfig, get_peft_model\n\nmodel_name = \"Qwen/Qwen2.5-3B-Instruct\"\n\ntokenizer = AutoTokenizer.from_pretrained(model_name)\nif tokenizer.pad_token is None:\n tokenizer.pad_token = tokenizer.eos_token\n\nmodel = AutoModelForCausalLM.from_pretrained(\n model_name,\n torch_dtype=torch.bfloat16,\n device_map=\"auto\",\n)\n\nlora_config = LoraConfig(\n r=16,\n lora_alpha=16,\n lora_dropout=0,\n target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\",\n \"gate_proj\", \"up_proj\", \"down_proj\"],\n task_type=\"CAUSAL_LM\",\n)\n\nmodel = get_peft_model(model, lora_config)\nmodel.print_trainable_parameters()"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  },
149
  {
150
  "cell_type": "code",
151
+ "execution_count": null,
152
  "metadata": {
153
  "colab": {
154
  "base_uri": "https://localhost:8080/"
 
156
  "id": "D3HDc8_3FG8L",
157
  "outputId": "b28882c2-1497-4d3b-98b8-4b4cb03cbb0b"
158
  },
159
+ "outputs": [],
160
+ "source": "# Cell 3: Game engine + constants + prompt builder\nimport random\nimport torch.nn.functional as F\n\n# Download game engine from repo\n!wget -q -O game_engine.py https://raw.githubusercontent.com/OutOfMystic/tetris-openenv/main/src/tetris_env/server/game_engine.py\nfrom game_engine import TetrisEnv\n\n# === Constants ===\nMAX_ACTIONS_PER_PIECE = 20 # max tokens model can output per piece\nMAX_STEPS_PER_GAME = 200 # max total actions per game\nGAMES_PER_ITER = 8 # games per training iteration (same seed)\nNUM_ITERATIONS = 100 # total training iterations\nTEMPERATURE = 0.7\n\n# Training reward modifiers (on top of engine per-step rewards)\nLR_PENALTY = -0.1 # per L/R move\nPIECE_PLACED_BONUS = 1.0 # per piece successfully placed\nNO_PLACE_PENALTY = -10.0 # if 20 tokens exhausted without placing\nLINE_CLEAR_BONUS = 100.0 # per line cleared (+100/+200/+300/+400)\n\n# Action mapping\nACTION_CHARS = ['L', 'R', 'C', 'W', 'D', 'S']\nACTION_TO_ENGINE = {\n 'L': 'left', 'R': 'right', 'C': 'rotate_cw',\n 'W': 'rotate_ccw', 'D': 'drop', 'S': 'down'\n}\n\n# Pre-compute token IDs for action characters\nACTION_TOKEN_IDS = []\nfor ch in ACTION_CHARS:\n ids = tokenizer.encode(ch, add_special_tokens=False)\n ACTION_TOKEN_IDS.append(ids[0])\n print(f\" '{ch}' -> token_id {ids[0]}\")\nACTION_TOKEN_IDS = torch.tensor(ACTION_TOKEN_IDS, device=model.device)\n\n# Token ID -> action char lookup\nTOKEN_TO_CHAR = {ACTION_TOKEN_IDS[i].item(): ACTION_CHARS[i] for i in range(6)}\n\nSYSTEM_PROMPT = \"\"\"You are a Tetris AI. You see the board and current piece.\nOutput actions as single letters: L=left R=right C=rotate_cw W=rotate_ccw D=drop S=down\nPlace the piece to fill complete rows. Drop when positioned.\"\"\"\n\ndef build_prompt(result):\n return f\"\"\"Board:\n{result['board']}\n\nPiece: {result['current_piece']} Next: {result['next_piece']}\nScore: {result['score']} Lines: {result['total_lines']} Height: {result['max_height']} Holes: {result['holes']}\n\nYour actions:\"\"\"\n\nprint(\"\\nGame engine loaded. Action tokens mapped.\")"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  },
162
  {
163
  "cell_type": "code",
164
+ "execution_count": null,
165
  "metadata": {
166
  "colab": {
167
  "base_uri": "https://localhost:8080/"
 
169
  "id": "3O3yawZBFG8M",
170
  "outputId": "7d8ed74b-4c46-4857-c1b7-9bb99848a93f"
171
  },
172
+ "outputs": [],
173
+ "source": "# Cell 4: Core training functions — play_one_game + train_one_iteration\n\ndef play_one_game(model, tokenizer, seed, temperature=TEMPERATURE):\n \"\"\"\n Play a full Tetris game. Model sees board before each piece,\n generates up to 20 action tokens per piece via autoregressive sampling.\n Returns reward + piece data for gradient computation.\n \"\"\"\n env = TetrisEnv(seed=seed)\n env.reset(seed=seed)\n\n # Initial random offset (same as old prompt generation)\n rng = random.Random(seed)\n moves = rng.randint(0, 4)\n direction = rng.choice([\"left\", \"right\"])\n for _ in range(moves):\n if env.done:\n break\n env.step(direction)\n\n total_reward = 0.0\n total_steps = 0\n pieces_data = []\n\n while not env.done and total_steps < MAX_STEPS_PER_GAME:\n current_piece_name = env.current_piece_name\n lines_before = env.total_lines\n\n # Build fresh prompt for this piece (no history)\n result = env._make_result(0)\n messages = [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": build_prompt(result)},\n ]\n prompt_ids = tokenizer.apply_chat_template(\n messages, return_tensors=\"pt\", add_generation_prompt=True\n ).to(model.device)\n\n # Autoregressive generation with KV cache\n input_ids = prompt_ids\n action_ids_list = []\n piece_placed = False\n past_kv = None\n\n with torch.no_grad():\n for _ in range(MAX_ACTIONS_PER_PIECE):\n if past_kv is None:\n out = model(input_ids, use_cache=True)\n past_kv = out.past_key_values\n else:\n out = model(input_ids[:, -1:], past_key_values=past_kv, use_cache=True)\n past_kv = out.past_key_values\n\n logits = out.logits[:, -1, :]\n\n # Mask to only 6 action tokens\n masked = torch.full_like(logits, float('-inf'))\n masked[0, ACTION_TOKEN_IDS] = logits[0, ACTION_TOKEN_IDS]\n probs = F.softmax(masked / temperature, dim=-1)\n\n token_id = torch.multinomial(probs, 1).item()\n action_ids_list.append(token_id)\n\n # Execute in engine\n action_char = TOKEN_TO_CHAR[token_id]\n action_name = ACTION_TO_ENGINE[action_char]\n step_result = env.step(action_name)\n\n total_reward += step_result['reward']\n total_steps += 1\n\n if action_name in ('left', 'right'):\n total_reward += LR_PENALTY\n\n # Piece placed? (new piece spawned)\n if env.current_piece_name != current_piece_name:\n piece_placed = True\n total_reward += PIECE_PLACED_BONUS\n lines_cleared = env.total_lines - lines_before\n if lines_cleared > 0:\n total_reward += lines_cleared * LINE_CLEAR_BONUS\n break\n\n if env.done:\n break\n\n # Append token for next autoregressive step\n input_ids = torch.cat([\n input_ids,\n torch.tensor([[token_id]], device=model.device)\n ], dim=-1)\n\n # Force drop if piece not placed in 20 tokens\n if not piece_placed and not env.done:\n env.step('drop')\n total_reward += NO_PLACE_PENALTY\n total_steps += 1\n lines_cleared = env.total_lines - lines_before\n if lines_cleared > 0:\n total_reward += lines_cleared * LINE_CLEAR_BONUS\n\n # Store for gradient computation\n if action_ids_list:\n pieces_data.append({\n 'prompt_ids': prompt_ids.cpu(),\n 'action_ids': torch.tensor(action_ids_list, dtype=torch.long),\n })\n\n return {\n 'reward': total_reward,\n 'pieces': pieces_data,\n 'total_steps': total_steps,\n 'total_lines': env.total_lines,\n 'pieces_placed': len(pieces_data),\n }\n\n\ndef train_one_iteration(model, optimizer, seed, temperature=TEMPERATURE):\n \"\"\"\n One GRPO iteration:\n 1. Rollout: play 8 games (same seed) without grad\n 2. Compute GRPO advantages from game rewards\n 3. Update: recompute log_probs with grad, policy gradient backward\n \"\"\"\n # Phase 1: Rollout\n games = []\n for _ in range(GAMES_PER_ITER):\n game = play_one_game(model, tokenizer, seed, temperature)\n games.append(game)\n\n rewards = torch.tensor([g['reward'] for g in games], dtype=torch.float32)\n mean_r = rewards.mean().item()\n std_r = rewards.std().item()\n\n # Phase 2: Advantages\n if std_r < 1e-8:\n return {'mean_reward': mean_r, 'std_reward': 0.0, 'loss': 0.0,\n 'avg_steps': sum(g['total_steps'] for g in games) / GAMES_PER_ITER,\n 'avg_lines': sum(g['total_lines'] for g in games) / GAMES_PER_ITER,\n 'avg_pieces': sum(g['pieces_placed'] for g in games) / GAMES_PER_ITER}\n\n advantages = ((rewards - rewards.mean()) / (rewards.std() + 1e-8)).tolist()\n\n # Phase 3: Update — recompute log_probs with grad, backward per piece\n optimizer.zero_grad()\n total_pieces = max(1, sum(len(g['pieces']) for g in games))\n total_loss = 0.0\n\n for game_idx, game in enumerate(games):\n adv = advantages[game_idx]\n for piece in game['pieces']:\n prompt = piece['prompt_ids'].to(model.device)\n actions = piece['action_ids'].to(model.device)\n if len(actions) == 0:\n continue\n\n # Teacher-forced forward pass: prompt + action tokens\n full_input = torch.cat([prompt.squeeze(0), actions]).unsqueeze(0)\n logits = model(full_input).logits\n\n # Log_probs at positions where actions were generated\n P = prompt.shape[-1]\n action_logits = logits[0, P-1 : P-1+len(actions), :]\n\n # Mask to 6 action tokens, apply temperature\n masked = torch.full_like(action_logits, float('-inf'))\n masked[:, ACTION_TOKEN_IDS] = action_logits[:, ACTION_TOKEN_IDS]\n log_probs = F.log_softmax(masked / temperature, dim=-1)\n\n selected = log_probs.gather(1, actions.unsqueeze(1)).squeeze(1)\n piece_loss = -(selected.sum() * adv) / total_pieces\n\n piece_loss.backward()\n total_loss += piece_loss.item()\n\n torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n optimizer.step()\n\n return {\n 'mean_reward': mean_r,\n 'std_reward': std_r,\n 'loss': total_loss,\n 'avg_steps': sum(g['total_steps'] for g in games) / GAMES_PER_ITER,\n 'avg_lines': sum(g['total_lines'] for g in games) / GAMES_PER_ITER,\n 'avg_pieces': sum(g['pieces_placed'] for g in games) / GAMES_PER_ITER,\n }\n\n# Smoke test\nprint(\"Smoke test: playing 1 game...\")\ntest_game = play_one_game(model, tokenizer, seed=0)\nprint(f\"Reward: {test_game['reward']:.1f}, Steps: {test_game['total_steps']}, \"\n f\"Pieces: {test_game['pieces_placed']}, Lines: {test_game['total_lines']}\")\nprint(\"Training functions ready.\")"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  },
175
  {
176
  "cell_type": "code",
177
+ "execution_count": null,
178
  "metadata": {
179
  "colab": {
180
  "base_uri": "https://localhost:8080/"
 
182
  "id": "JdcKswz5FG8M",
183
  "outputId": "b0533b78-b10a-4d09-bea1-1b18d8a58c54"
184
  },
185
+ "outputs": [],
186
+ "source": "# Cell 5: Demo — UNTRAINED model plays one game\nprint(\"=== UNTRAINED MODEL ===\\n\")\n\ngame = play_one_game(model, tokenizer, seed=42)\n\nprint(f\"Total steps: {game['total_steps']}/{MAX_STEPS_PER_GAME}\")\nprint(f\"Pieces placed: {game['pieces_placed']}\")\nprint(f\"Lines cleared: {game['total_lines']}\")\nprint(f\"Game reward: {game['reward']:+.1f}\")\n\n# Show final board by replaying\nenv_demo = TetrisEnv(seed=42)\nenv_demo.reset(seed=42)\nrng = random.Random(42)\nm = rng.randint(0, 4)\nd = rng.choice([\"left\", \"right\"])\nfor _ in range(m):\n if not env_demo.done:\n env_demo.step(d)\nfor piece in game['pieces']:\n for tid in piece['action_ids'].tolist():\n if not env_demo.done:\n env_demo.step(ACTION_TO_ENGINE[TOKEN_TO_CHAR[tid]])\n # Force drop for pieces that weren't placed\n if not env_demo.done and env_demo.current_piece_name == env_demo.current_piece_name:\n pass # engine handles lock internally via gravity\n\nprint(f\"\\nFinal board:\")\nprint(env_demo.board_to_text())\n\nuntrained_reward = game['reward']\nprint(f\"\\nUntrained reward: {untrained_reward:+.1f}\")"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  },
188
  {
189
  "cell_type": "code",
190
+ "source": "# Cell 6: Training loop 100 iterations of per-piece GRPO\noptimizer = torch.optim.AdamW(\n [p for p in model.parameters() if p.requires_grad],\n lr=5e-6,\n weight_decay=0.01,\n)\n\nhistory = []\n\nprint(\"Starting per-piece GRPO training...\")\nprint(f\"Config: {GAMES_PER_ITER} games/iter, max {MAX_STEPS_PER_GAME} steps/game, \"\n f\"max {MAX_ACTIONS_PER_PIECE} tokens/piece, {NUM_ITERATIONS} iterations\\n\")\n\nfor iteration in range(NUM_ITERATIONS):\n stats = train_one_iteration(model, optimizer, seed=iteration)\n history.append(stats)\n\n if iteration % 5 == 0 or iteration == NUM_ITERATIONS - 1:\n print(f\"[Iter {iteration:3d}] \"\n f\"reward={stats['mean_reward']:+8.1f} \"\n f\"std={stats['std_reward']:6.1f} \"\n f\"loss={stats['loss']:7.3f} \"\n f\"steps={stats['avg_steps']:5.1f} \"\n f\"lines={stats['avg_lines']:4.1f} \"\n f\"pieces={stats['avg_pieces']:4.1f}\")\n\nprint(\"\\nTraining complete!\")",
191
  "metadata": {},
192
  "execution_count": null,
193
  "outputs": []
194
  },
195
  {
196
  "cell_type": "code",
197
+ "execution_count": null,
198
  "metadata": {
199
  "colab": {
200
  "base_uri": "https://localhost:8080/"
 
202
  "id": "1SRpxwy-FG8M",
203
  "outputId": "30c168c0-faf0-4c26-fd74-62d3ed3ab13b"
204
  },
205
+ "outputs": [],
206
+ "source": "# Cell 7: Plot reward curve\nimport matplotlib.pyplot as plt\n\nfig, axes = plt.subplots(1, 3, figsize=(18, 5))\n\niters = range(len(history))\n\naxes[0].plot(iters, [h['mean_reward'] for h in history])\naxes[0].set_title('Mean Reward per Iteration')\naxes[0].set_xlabel('Iteration')\naxes[0].set_ylabel('Reward')\n\naxes[1].plot(iters, [h['avg_lines'] for h in history])\naxes[1].set_title('Avg Lines Cleared')\naxes[1].set_xlabel('Iteration')\n\naxes[2].plot(iters, [h['loss'] for h in history])\naxes[2].set_title('Policy Gradient Loss')\naxes[2].set_xlabel('Iteration')\n\nplt.tight_layout()\nplt.savefig('reward_curve.png', dpi=150)\nplt.show()\nprint(\"Reward curve saved to reward_curve.png\")"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  },
208
  {
209
  "cell_type": "code",
 
216
  "id": "UKvvm6gPFG8M",
217
  "outputId": "413432d5-f511-40d5-eb43-0874012e593d"
218
  },
219
+ "outputs": [],
220
+ "source": "# Cell 8: Demo — TRAINED model plays 3 games\nprint(\"=== TRAINED MODEL ===\\n\")\n\ntrained_rewards = []\nfor seed in [42, 123, 7]:\n game = play_one_game(model, tokenizer, seed=seed)\n print(f\"Seed {seed}: reward={game['reward']:+.1f}, \"\n f\"steps={game['total_steps']}, lines={game['total_lines']}, \"\n f\"pieces={game['pieces_placed']}\")\n trained_rewards.append(game['reward'])\n\navg_trained = sum(trained_rewards) / len(trained_rewards)\nprint(f\"\\n{'='*50}\")\nprint(f\"UNTRAINED reward (seed=42): {untrained_reward:+.1f}\")\nprint(f\"TRAINED avg reward (3 games): {avg_trained:+.1f}\")\nprint(f\"Improvement: {avg_trained - untrained_reward:+.1f}\")\nprint('='*50)\n\nif avg_trained > untrained_reward:\n print(\"Training improved the model!\")\nelse:\n print(\"Model needs more training or tuning.\")"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  },
222
  {
223
  "cell_type": "code",
224
+ "source": "# Cell 9: Push trained model to HF Hub\nmodel.push_to_hub(\"VortexedSquirrel/tetris-agent-grpo\")\ntokenizer.push_to_hub(\"VortexedSquirrel/tetris-agent-grpo\")\nprint(\"Model pushed to https://huggingface.co/VortexedSquirrel/tetris-agent-grpo\")",
225
  "metadata": {},
226
  "execution_count": null,
227
  "outputs": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  }
229
  ],
230
  "metadata": {