Harness context codex item completed
{
"type": "codex_item_completed",
"item": {
"type": "CommandExecution",
"id": "exec-91ad6a98-7bb6-4ef4-b691-ce330ea44f90",
"process_id": "65440",
"command": [
"/bin/sh",
"-lc",
"sed -n '300,470p' /task/current_model/two_tower_model.py; sed -n '560,780p' /task/current_model/train_current_model.py; sed -n '800,900p' /task/current_model/train_current_model.py; sed -n '1510,1790p' /task/current_model/train_current_model.py; sed -n '1860,2160p' /task/current_model/train_current_model.py"
],
"cwd": "file:///app",
"parsed_cmd": [
{
"type": "read",
"cmd": "sed -n '300,470p' /task/current_model/two_tower_model.py",
"name": "two_tower_model.py",
"path": "/task/current_model/two_tower_model.py"
},
{
"type": "read",
"cmd": "sed -n '560,780p' /task/current_model/train_current_model.py",
"name": "train_current_model.py",
"path": "/task/current_model/train_current_model.py"
},
{
"type": "read",
"cmd": "sed -n '800,900p' /task/current_model/train_current_model.py",
"name": "train_current_model.py",
"path": "/task/current_model/train_current_model.py"
},
{
"type": "read",
"cmd": "sed -n '1510,1790p' /task/current_model/train_current_model.py",
"name": "train_current_model.py",
"path": "/task/current_model/train_current_model.py"
},
{
"type": "read",
"cmd": "sed -n '1860,2160p' /task/current_model/train_current_model.py",
"name": "train_current_model.py",
"path": "/task/current_model/train_current_model.py"
}
],
"source": "unified_exec_startup",
"status": "completed",
"stdout": " input_size=config.hidden_dim,\n hidden_size=dim,\n num_layers=1,\n batch_first=True,\n )\n self.user_tower = nn.Sequential(\n nn.Linear(dim * 3 + 6, config.user_hidden_1),\n nn.GELU(),\n nn.Dropout(config.dropout),\n nn.Linear(config.user_hidden_1, config.user_hidden_2),\n nn.GELU(),\n nn.Dropout(config.dropout),\n nn.Linear(config.user_hidden_2, dim),\n )\n self.candidate_embedding = nn.Embedding(n_items, dim)\n self.register_buffer(\n \"item_features\", torch.from_numpy(item_features), persistent=True\n )\n self.item_tower = nn.Sequential(\n nn.Linear(dim + n_item_features, config.item_hidden_1),\n nn.GELU(),\n nn.Dropout(config.dropout),\n nn.Linear(config.item_hidden_1, config.item_hidden_2),\n nn.GELU(),\n nn.Dropout(config.dropout),\n nn.Linear(config.item_hidden_2, dim),\n )\n nn.init.normal_(self.context_embedding.weight, std=0.02)\n nn.init.normal_(self.candidate_embedding.weight, std=0.02)\n with torch.no_grad():\n self.context_embedding.weight[n_items].zero_()\n\n def _mean_context_channel(self, items: torch.Tensor) -> torch.Tensor:\n mask = items.ne(self.n_items)\n vectors = self.context_embedding(items)\n return (vectors * mask.unsqueeze(-1)).sum(dim=1) / mask.sum(\n dim=1, keepdim=True\n ).clamp_min(1)\n\n def encode_items(self, items: torch.Tensor) -> torch.Tensor:\n inputs = torch.cat(\n [self.candidate_embedding(items), self.item_features[items]], dim=1\n )\n return self.item_tower(inputs)\n\n\ndef invalid_negative_mask(\n negatives: torch.Tensor,\n targets: torch.Tensor,\n context_items: torch.Tensor,\n liked_items: torch.Tensor,\n) -> torch.Tensor:\n \"\"\"Identify targets, known positives, active likes, and row duplicates.\"\"\"\n invalid = negatives.eq(targets[:, None])\n invalid |= negatives[:, :, None].eq(context_items[:, None, :]).any(dim=2)\n invalid |= negatives[:, :, None].eq(liked_items[:, None, :]).any(dim=2)\n for column in range(1, negatives.shape[1]):\n invalid[:, column] |= negatives[:, column, None].eq(\n negatives[:, :column]\n ).any(dim=1)\n return invalid\n\n\ndef draw_filtered_mixed_negatives(\n targets: torch.Tensor,\n context_items: torch.Tensor,\n liked_items: torch.Tensor,\n n_items: int,\n uniform_count: int,\n inbatch_count: int,\n item_correction: torch.Tensor,\n generator: torch.Generator,\n device: torch.device,\n) -> tuple[torch.Tensor, torch.Tensor]:\n \"\"\"Draw uniform and unique-target in-batch negatives.\"\"\"\n batch_size = len(targets)\n uniform = torch.randint(\n n_items,\n (batch_size, uniform_count),\n generator=generator,\n device=\"cpu\",\n )\n inbatch = torch.empty((batch_size, 0), dtype=torch.long, device=\"cpu\")\n corrected_count = 0\n if inbatch_count:\n unique_targets = torch.unique(targets.detach().cpu())\n corrected_count = min(inbatch_count, max(len(unique_targets) - 1, 0))\n if corrected_count:\n priorities = torch.rand(\n (batch_size, len(unique_targets)), generator=generator\n )\n priorities.masked_fill_(\n unique_targets[None, :].eq(targets.detach().cpu()[:, None]),\n -1.0,\n )\n columns = torch.topk(priorities, k=corrected_count, dim=1).indices\n inbatch = unique_targets[columns]\n if corrected_count < inbatch_count:\n filler = torch.randint(\n n_items,\n (batch_size, inbatch_count - corrected_count),\n generator=generator,\n device=\"cpu\",\n )\n inbatch = torch.cat([inbatch, filler], dim=1)\n\n negatives_cpu = torch.cat([uniform, inbatch], dim=1)\n correction_cpu = torch.ones_like(negatives_cpu, dtype=torch.float32)\n if corrected_count:\n correction_cpu[\n :, uniform_count : uniform_count + corrected_count\n ] = item_correction[inbatch[:, :corrected_count]]\n negatives = negatives_cpu.to(device)\n correction = correction_cpu.to(device)\n for _ in range(32):\n invalid = invalid_negative_mask(\n negatives, targets, context_items, liked_items\n )\n if not bool(invalid.any()):\n return negatives, correction\n replacements = torch.randint(\n n_items,\n (int(invalid.sum().detach().cpu()),),\n generator=generator,\n device=\"cpu\",\n ).to(device)\n negatives[invalid] = replacements\n correction[invalid] = 1.0\n invalid = invalid_negative_mask(\n negatives, targets, context_items, liked_items\n )\n if bool(invalid.any()):\n raise RuntimeError(\"mixed negative resampling failed false-negative filter\")\n return negatives, correction\n raw = np.power(\n (1.0 / len(counts)) / probabilities,\n config.inbatch_correction_power,\n )\n normalization = float(np.sum(counts * raw) / max(total, 1.0))\n correction = np.clip(\n raw / max(normalization, 1.0e-12),\n config.inbatch_correction_min,\n config.inbatch_correction_max,\n ).astype(np.float32)\n mean_on_targets = float(np.sum(counts * correction) / max(total, 1.0))\n log(\n f\"inbatch correction: min={correction.min():.3f} \"\n f\"mean_on_targets={mean_on_targets:.3f} max={correction.max():.3f}\"\n )\n return torch.from_numpy(correction)\n\n\ndef build_known_positive_filter(\n prepared: SequenceData, training_counts: np.ndarray\n) -> np.ndarray:\n \"\"\"Pack each user's complete public-training positive-item set.\"\"\"\n width = (len(prepared.item_ids) + 7) // 8\n packed = np.zeros((len(prepared.user_ids), width), dtype=np.uint8)\n retained = 0\n for user_row, target_count in enumerate(training_counts):\n if not target_count:\n continue\n offset = int(prepared.offsets[user_row])\n # T adjacent targets consume sequence positions 0 through T. The\n # chronological BCE suffix starts at T + 1 and is intentionally absent.\n items = np.unique(\n prepared.sequence_items[\n offset : offset + int(target_count) + 1\n ].astype(np.int64)\n )\n byte_columns = items >> 3\n bit_values = np.left_shift(\n np.uint8(1), (items & 7).astype(np.uint8)\n )\n np.bitwise_or.at(packed[user_row], byte_columns, bit_values)\n retained += len(items)\n log(\n f\"full-history false-negative filter: users={len(prepared.user_ids):,} \"\n f\"unique_user_items={retained:,} bytes={packed.nbytes:,}\"\n )\n return packed\n\n\ndef invalid_full_history_negatives(\n negatives: np.ndarray,\n targets: np.ndarray,\n user_rows: np.ndarray,\n known_positive_packed: np.ndarray,\n liked_items: np.ndarray,\n) -> np.ndarray:\n \"\"\"Return row-level invalid negatives using the complete training prefix.\"\"\"\n byte_columns = negatives >> 3\n bit_columns = negatives & 7\n known = known_positive_packed[user_rows[:, None], byte_columns]\n invalid = ((known >> bit_columns) & 1).astype(bool)\n invalid |= negatives == targets[:, None]\n invalid |= (negatives[:, :, None] == liked_items[:, None, :]).any(axis=2)\n for column in range(1, negatives.shape[1]):\n invalid[:, column] |= (\n negatives[:, column, None] == negatives[:, :column]\n ).any(axis=1)\n return invalid\n\n\ndef draw_full_history_filtered_negatives(\n targets: torch.Tensor,\n user_rows: np.ndarray,\n liked_items: torch.Tensor,\n known_positive_packed: np.ndarray,\n n_items: int,\n config: Config,\n item_correction: torch.Tensor,\n generator: torch.Generator,\n device: torch.device,\n) -> tuple[torch.Tensor, torch.Tensor]:\n \"\"\"Draw mixed negatives and reject all known public-training positives.\"\"\"\n targets_cpu = targets.detach().cpu().numpy().astype(np.int64, copy=False)\n liked_cpu = liked_items.detach().cpu().numpy().astype(np.int64, copy=False)\n batch_size = len(targets_cpu)\n uniform = torch.randint(\n n_items,\n (batch_size, config.uniform_negatives),\n generator=generator,\n device=\"cpu\",\n )\n inbatch = torch.empty((batch_size, 0), dtype=torch.long)\n corrected_count = 0\n if config.inbatch_negatives:\n unique_targets = torch.unique(targets.detach().cpu())\n corrected_count = min(\n config.inbatch_negatives, max(len(unique_targets) - 1, 0)\n )\n if corrected_count:\n priorities = torch.rand(\n (batch_size, len(unique_targets)), generator=generator\n )\n priorities.masked_fill_(\n unique_targets[None, :].eq(\n torch.from_numpy(targets_cpu)[:, None]\n ),\n -1.0,\n )\n columns = torch.topk(\n priorities, k=corrected_count, dim=1\n ).indices\n inbatch = unique_targets[columns]\n if corrected_count < config.inbatch_negatives:\n filler = torch.randint(\n n_items,\n (batch_size, config.inbatch_negatives - corrected_count),\n generator=generator,\n device=\"cpu\",\n )\n inbatch = torch.cat([inbatch, filler], dim=1)\n\n negatives = torch.cat([uniform, inbatch], dim=1).numpy()\n correction = np.ones_like(negatives, dtype=np.float32)\n if corrected_count:\n correction[\n :, config.uniform_negatives : config.uniform_negatives + corrected_count\n ] = item_correction[inbatch[:, :corrected_count]].numpy()\n for _ in range(32):\n invalid = invalid_full_history_negatives(\n negatives,\n targets_cpu,\n user_rows,\n known_positive_packed,\n liked_cpu,\n )\n if not np.any(invalid):\n return (\n torch.from_numpy(negatives).to(device),\n torch.from_numpy(correction).to(device),\n )\n replacements = torch.randint(\n n_items,\n (int(invalid.sum()),),\n generator=generator,\n device=\"cpu\",\n ).numpy()\n negatives[invalid] = replacements\n # Rejected in-batch items become ordinary uniform draws.\n correction[invalid] = 1.0\n invalid = invalid_full_history_negatives(\n negatives,\n targets_cpu,\n user_rows,\n known_positive_packed,\n liked_cpu,\n )\n if np.any(invalid):\n raise RuntimeError(\"full-history negative resampling failed\")\n return (\n torch.from_numpy(negatives).to(device),\n torch.from_numpy(correction).to(device),\n )\n\n\ndef draw_training_negatives(\n positive_items: torch.Tensor,\n known_history: torch.Tensor,\n liked_items: torch.Tensor,\n user_rows: np.ndarray,\n known_positive_packed: np.ndarray | None,\n prepared: SequenceData,\n config: Config,\n item_correction: torch.Tensor,\n generator: torch.Generator,\n device: torch.device,\n) -> tuple[torch.Tensor, torch.Tensor]:\n if config.negative_filter_scope == \"full-training-history\":\n if known_positive_packed is None:\n raise RuntimeError(\"full-history negative filter was not built\")\n return draw_full_history_filtered_negatives(\n positive_items,\n user_rows,\n liked_items,\n known_positive_packed,\n len(prepared.item_ids),\n config,\n item_correction,\n generator,\n device,\n )\n return base.draw_filtered_mixed_negatives(\n positive_items,\n known_history,\n liked_items,\n len(prepared.item_ids),\n config.uniform_negatives,\n config.inbatch_negatives,\n item_correction,\n generator,\n device,\n )\n\n\ndef event_sequence(\n model: base.SequentialTwoTower,\n items: torch.Tensor,\n features: torch.Tensor,\n hidden: torch.Tensor,\n) -> tuple[torch.Tensor, torch.Tensor]:\n inputs = torch.cat([model.context_embedding(items), features], dim=-1)\n projected = model.event_projection(inputs)\n return model.sequence_encoder(projected, hidden)\n\n\ndef pool_explicit_states(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n state_ids: np.ndarray,\n device: torch.device,\n) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:\n unique_states, inverse = np.unique(state_ids, return_inverse=True)\n sequence_vectors: torch.Tensor,\n liked_vectors: torch.Tensor,\n disliked_vectors: torch.Tensor,\n user_features: torch.Tensor,\n) -> torch.Tensor:\n return model.user_tower(\n torch.cat(\n [sequence_vectors, liked_vectors, disliked_vectors, user_features],\n dim=1,\n )\n )\n\n\ndef candidate_vectors(\n model: base.SequentialTwoTower,\n positives: torch.Tensor,\n negatives: torch.Tensor,\n) -> tuple[torch.Tensor, torch.Tensor]:\n all_items = torch.cat([positives, negatives.reshape(-1)])\n unique_items, inverse = torch.unique(all_items, return_inverse=True)\n encoded = model.encode_items(unique_items)\n positive_vectors = encoded[inverse[: len(positives)]]\n negative_vectors = encoded[inverse[len(positives) :]].reshape(\n len(positives), negatives.shape[1], -1\n )\n return positive_vectors, negative_vectors\n\n\ndef history_windows(\n sequence: np.ndarray,\n start: int,\n count: int,\n width: int,\n padding_item: int,\n) -> np.ndarray:\n base_start = max(0, start - width + 1)\n segment = sequence[base_start : start + count]\n padded = np.concatenate(\n [np.full(width - 1, padding_item, dtype=np.uint16), segment]\n )\n windows = np.lib.stride_tricks.sliding_window_view(padded, width)\n offset = start - base_start\n return windows[offset : offset + count].copy()\n\n\ndef corrected_negative_loss(\n logits: torch.Tensor,\n correction: torch.Tensor,\n config: Config,\n) -> torch.Tensor:\n raw = F.softplus(logits)\n total = config.uniform_negatives + config.inbatch_negatives\n result = torch.zeros(len(logits), device=logits.device)\n if config.uniform_negatives:\n result += config.uniform_negatives * raw[\n :, : config.uniform_negatives\n ].mean(dim=1)\n if config.inbatch_negatives:\n selected = slice(config.uniform_negatives, total)\n weights = correction[:, selected]\n result += config.inbatch_negatives * (\n (raw[:, selected] * weights).sum(dim=1)\n / weights.sum(dim=1).clamp_min(1.0e-6)\n )\n return result / total\n\n\ndef retained_target_window_starts(\n total_targets: np.ndarray,\n validation_counts: np.ndarray,\n training_counts: np.ndarray,\n) -> np.ndarray:\n \"\"\"Return the discarded prefix before the retained chronological suffix.\"\"\"\n training_region_ends = total_targets - validation_counts\n window_starts = training_region_ends - training_counts\n if (\n np.any(window_starts < 0)\n or np.any(training_counts < 0)\n or np.any(validation_counts < 0)\n ):\n raise RuntimeError(\"chronological target partition produced negative counts\")\n if not np.array_equal(\n window_starts + training_counts + validation_counts, total_targets\n ):\n raise RuntimeError(\"chronological target window is not a final contiguous suffix\")\n return window_starts\n\n\ndef chronological_target_partition(\n prepared: SequenceData,\n validation_fraction: float,\n max_train_targets_per_user: int,\n max_validation_targets_per_user: int,\n) -> TargetPartition:\n \"\"\"Select a recent contiguous window with a final out-of-time BCE suffix.\"\"\"\n totals = np.maximum(prepared.lengths - 1, 0).astype(np.int64)\n validation = np.floor(totals * validation_fraction).astype(np.int64)\n validation[(totals >= 2) & (validation == 0)] = 1\n if max_validation_targets_per_user:\n validation = np.minimum(validation, max_validation_targets_per_user)\n\n \"diagnostics\": diagnostics,\n \"model_state\": model_state,\n \"optimizer_state\": optimizer.state_dict(),\n \"generator_state\": generator.get_state(),\n },\n path,\n )\n log(f\"saved epoch checkpoint: {path}\")\n return path\n\n\ndef train_model(\n public: Path,\n stage: str,\n prepared: SequenceData,\n config: Config,\n device: torch.device,\n checkpoint_dir: Path | None,\n resume_checkpoint: Path | None,\n) -> tuple[base.SequentialTwoTower, list[dict[str, float]]]:\n partition = chronological_target_partition(\n prepared,\n config.validation_fraction,\n config.max_train_targets_per_user,\n config.max_validation_targets_per_user,\n )\n training_counts = partition.training_counts\n validation_counts = partition.validation_counts\n log(\n f\"chronological BCE window: train={int(training_counts.sum()):,} \"\n f\"validation={int(validation_counts.sum()):,} \"\n f\"selected={partition.selected_count:,} \"\n f\"discarded_old_targets={int(partition.window_starts.sum()):,} \"\n f\"all={prepared.target_count:,}\"\n )\n model = base.SequentialTwoTower(prepared.item_features, model_config(config)).to(\n device\n )\n optimizer = torch.optim.AdamW(\n model.parameters(), lr=config.learning_rate, weight_decay=config.weight_decay\n )\n generator = torch.Generator(device=\"cpu\").manual_seed(config.seed + 10_000)\n item_correction = build_item_correction(\n prepared,\n config,\n partition.window_starts,\n training_counts,\n )\n target_weights = build_target_weights(\n public,\n stage,\n prepared,\n partition.history_training_counts,\n config,\n )\n known_positive_packed = (\n build_known_positive_filter(prepared, partition.history_training_counts)\n if config.negative_filter_scope == \"full-training-history\"\n else None\n )\n history: list[dict[str, float]] = []\n best_loss = float(\"inf\")\n best_epoch = 0\n best_state: dict[str, torch.Tensor] | None = None\n epochs_without_improvement = 0\n start_epoch = 0\n\n if resume_checkpoint is not None:\n payload = torch.load(\n resume_checkpoint, map_location=device, weights_only=False\n )\n stored_config = payload.get(\"config\", {})\n architecture_fields = (\n \"bptt_steps\",\n \"embedding_dim\",\n \"hidden_dim\",\n \"user_hidden_1\",\n \"user_hidden_2\",\n \"item_hidden_1\",\n \"item_hidden_2\",\n )\n mismatches = {\n name: (stored_config.get(name), getattr(config, name))\n for name in architecture_fields\n if stored_config.get(name) != getattr(config, name)\n }\n if mismatches:\n raise RuntimeError(\n f\"resume checkpoint architecture differs: {mismatches}\"\n )\n stored_filter_scope = stored_config.get(\n \"negative_filter_scope\", \"last-100\"\n )\n stored_target_weighting = stored_config.get(\n \"target_weighting\", \"uniform\"\n )\n stored_weight_values = (\n stored_config.get(\"ordinary_target_weight\", 1.0),\n stored_config.get(\"organic_strong_target_weight\", 1.0),\n stored_config.get(\"organic_like_target_weight\", 3.0),\n stored_config.get(\"discovery_horizon_days\", 7),\n )\n current_weight_values = (\n config.ordinary_target_weight,\n config.organic_strong_target_weight,\n config.organic_like_target_weight,\n config.discovery_horizon_days,\n )\n stored_window_values = (\n stored_config.get(\"validation_fraction\", 0.10),\n stored_config.get(\"max_train_targets_per_user\", 0),\n stored_config.get(\"max_validation_targets_per_user\", 0),\n )\n current_window_values = (\n config.validation_fraction,\n config.max_train_targets_per_user,\n config.max_validation_targets_per_user,\n )\n objective_changed = (\n stored_filter_scope != config.negative_filter_scope\n or stored_target_weighting != config.target_weighting\n or stored_weight_values != current_weight_values\n or stored_window_values != current_window_values\n )\n model.load_state_dict(payload[\"model_state\"])\n if \"optimizer_state\" in payload and not objective_changed:\n optimizer.load_state_dict(payload[\"optimizer_state\"])\n for group in optimizer.param_groups:\n group[\"lr\"] = config.learning_rate\n group[\"weight_decay\"] = config.weight_decay\n log(f\"restored optimizer state from {resume_checkpoint}\")\n else:\n log(\n f\"using a fresh AdamW optimizer after loading {resume_checkpoint}\"\n )\n if \"generator_state\" in payload and not objective_changed:\n generator.set_state(payload[\"generator_state\"])\n start_epoch = int(payload[\"epoch\"])\n if start_epoch >= config.epochs:\n raise RuntimeError(\n f\"resume epoch {start_epoch} must be below --epochs {config.epochs}\"\n )\n prior = {\n str(name): float(value)\n for name, value in payload.get(\"diagnostics\", {}).items()\n }\n if objective_changed:\n prior.pop(\"validation_loss\", None)\n log(\n \"BCE objective changed: filter \"\n f\"{stored_filter_scope}->{config.negative_filter_scope}, \"\n \"target weighting \"\n f\"{stored_target_weighting}->{config.target_weighting}, \"\n f\"target window {stored_window_values}->{current_window_values}; \"\n \"recomputing the resume checkpoint's validation BCE\"\n )\n if \"validation_loss\" not in prior:\n prior.update(\n evaluate_validation_bce(\n model,\n prepared,\n config,\n target_weights,\n partition.window_starts,\n training_counts,\n validation_counts,\n item_correction,\n known_positive_packed,\n device,\n )\n )\n history.append(prior)\n best_loss = float(prior[\"validation_loss\"])\n best_epoch = start_epoch\n best_state = snapshot_model(model)\n log(\n f\"resumed validation-BCE training: epoch={start_epoch} \"\n f\"loss={best_loss:.6f}\"\n )\n\n for epoch in range(start_epoch + 1, config.epochs + 1):\n diagnostics = run_training_epoch(\n model,\n optimizer,\n prepared,\n config,\n target_weights,\n partition.window_starts,\n training_counts,\n item_correction,\n known_positive_packed,\n generator,\n device,\n epoch,\n )\n validation = evaluate_validation_bce(\n model,\n prepared,\n config,\n target_weights,\n partition.window_starts,\n training_counts,\n validation_counts,\n item_correction,\n known_positive_packed,\n device,\n )\n diagnostics.update(validation)\n history.append(diagnostics)\n current_state = snapshot_model(model)\n if checkpoint_dir is not None:\n save_epoch_checkpoint(\n checkpoint_dir,\n epoch,\n current_state,\n config,\n diagnostics,\n optimizer,\n generator,\n )\n improved = (\n diagnostics[\"validation_loss\"]\n < best_loss - config.early_stopping_min_delta\n )\n if improved:\n best_loss = diagnostics[\"validation_loss\"]\n best_epoch = epoch\n best_state = current_state\n epochs_without_improvement = 0\n if checkpoint_dir is not None:\n torch.save(\n {\n \"epoch\": epoch,\n \"config\": asdict(config),\n \"diagnostics\": diagnostics,\n \"model_state\": current_state,\n \"optimizer_state\": optimizer.state_dict(),\n \"generator_state\": generator.get_state(),\n },\n checkpoint_dir / \"best.pt\",\n )\n else:\n epochs_without_improvement += 1\n log(\n f\"epoch={epoch}/{config.epochs} train_loss={diagnostics['loss']:.5f} \"\n f\"validation_loss={diagnostics['validation_loss']:.5f} \"\n f\"pair_accuracy={diagnostics['pair_accuracy']:.3f} \"\n f\"train_targets={int(diagnostics['targets']):,} \"\n f\"validation_targets={int(diagnostics['validation_targets']):,} \"\n f\"train_seconds={diagnostics['seconds']:.1f} \"\n f\"validation_seconds={diagnostics['validation_seconds']:.1f}\"\n )\n release_accelerator_cache(device)\n if epochs_without_improvement >= config.early_stopping_patience:\n log(\n f\"early stopping after epoch={epoch}; \"\n f\"best_epoch={best_epoch} best_validation_loss={best_loss:.6f}\"\n )\n break\n\n if best_state is None:\n raise RuntimeError(\"training did not produce a valid checkpoint\")\n model.load_state_dict(best_state)\n model.to(device).eval()\n log(\n f\"restored best validation-BCE checkpoint: epoch={best_epoch} \"\n f\"loss={best_loss:.6f}\"\n )\n return model, history\n\n\[email protected]_mode()\ndef inference_queries(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n config: Config,\n device: torch.device,\n) -> torch.Tensor:\n sequence_rows = prepared.inference_user_rows\n lengths = np.where(\n sequence_rows >= 0, prepared.lengths[np.maximum(sequence_rows, 0)], 0\[email protected]_mode()\ndef rank_candidates(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n eligibility: CandidateSet,\n config: Config,\n device: torch.device,\n ranking_batch_size: int,\n) -> tuple[pd.DataFrame, dict[str, float]]:\n model.eval()\n queries = inference_queries(model, prepared, config, device)\n item_parts: list[torch.Tensor] = []\n for start in range(0, len(prepared.item_ids), 2_048):\n rows = torch.arange(\n start,\n min(start + 2_048, len(prepared.item_ids)),\n dtype=torch.long,\n device=device,\n )\n item_parts.append(model.encode_items(rows).cpu())\n items = torch.cat(item_parts)\n rows: list[tuple[int, int, int]] = []\n for start in range(0, len(eligibility.uids), ranking_batch_size):\n end = min(start + ranking_batch_size, len(eligibility.uids))\n scores = queries[start:end] @ items.T\n eligible = np.unpackbits(\n eligibility.packed[start:end],\n axis=1,\n count=len(prepared.item_ids),\n bitorder=\"little\",\n ).astype(bool, copy=False)\n scores.masked_fill_(~torch.from_numpy(eligible), -torch.inf)\n selected = torch.topk(scores, k=base.TOP_N, dim=1).indices.numpy()\n for local_row, columns in enumerate(selected):\n uid = int(eligibility.uids[start + local_row])\n rows.extend(\n (uid, int(prepared.item_ids[column]), rank)\n for rank, column in enumerate(columns, start=1)\n )\n if end == len(eligibility.uids) or end % 1_024 == 0:\n log(f\"ranked users={end:,}/{len(eligibility.uids):,}\")\n query_norm = torch.linalg.vector_norm(queries, dim=1).numpy()\n item_norm = torch.linalg.vector_norm(items, dim=1).numpy()\n diagnostics = {\n \"query_norm_mean\": float(query_norm.mean()),\n \"query_norm_std\": float(query_norm.std()),\n \"item_norm_mean\": float(item_norm.mean()),\n \"item_norm_std\": float(item_norm.std()),\n }\n return pd.DataFrame(rows, columns=[\"uid\", \"item_id\", \"rank\"]), diagnostics\n\n\ndef write_ranking(frame: pd.DataFrame, output: Path) -> None:\n output.parent.mkdir(parents=True, exist_ok=True)\n if output.suffix == \".parquet\":\n frame.to_parquet(output, index=False, compression=\"zstd\")\n elif output.suffix == \".csv\":\n frame.to_csv(output, index=False)\n else:\n raise ValueError(\"--output must end in .parquet or .csv\")\n\n\ndef main() -> int:\n parser = argparse.ArgumentParser(description=__doc__)\n parser.add_argument(\n \"--stage\", choices=(\"validation\", \"submission\"), default=\"validation\"\n )\n parser.add_argument(\"--output\", type=Path, required=True)\n parser.add_argument(\"--public-dir\", type=Path, required=True)\n parser.add_argument(\"--prepared-cache\", type=Path)\n parser.add_argument(\"--device\", choices=(\"cpu\", \"mps\"), default=\"cpu\")\n parser.add_argument(\"--threads\", type=int, default=8)\n parser.add_argument(\"--bptt-steps\", type=int, default=Config.bptt_steps)\n parser.add_argument(\n \"--user-batch-size\", type=int, default=Config.user_batch_size\n )\n parser.add_argument(\"--epochs\", type=int, default=Config.epochs)\n parser.add_argument(\"--learning-rate\", type=float, default=Config.learning_rate)\n parser.add_argument(\n \"--loss-microbatch-size\",\n type=int,\n default=Config.loss_microbatch_size,\n )\n parser.add_argument(\n \"--negative-filter-scope\",\n choices=(\"full-training-history\", \"last-100\"),\n default=Config.negative_filter_scope,\n )\n parser.add_argument(\n \"--target-weighting\",\n choices=(\"discovery-graded\", \"organic-strong\", \"uniform\"),\n default=Config.target_weighting,\n )\n parser.add_argument(\n \"--ordinary-target-weight\",\n type=float,\n default=Config.ordinary_target_weight,\n )\n parser.add_argument(\n \"--organic-strong-target-weight\",\n type=float,\n default=Config.organic_strong_target_weight,\n )\n parser.add_argument(\n \"--organic-like-target-weight\",\n type=float,\n default=Config.organic_like_target_weight,\n )\n parser.add_argument(\n \"--discovery-horizon-days\",\n type=int,\n default=Config.discovery_horizon_days,\n )\n parser.add_argument(\n \"--validation-fraction\",\n type=float,\n default=Config.validation_fraction,\n )\n parser.add_argument(\n \"--max-train-targets-per-user\",\n type=int,\n default=Config.max_train_targets_per_user,\n help=\"retain this many recent pre-validation targets per user; 0 is uncapped\",\n )\n parser.add_argument(\n \"--max-validation-targets-per-user\",\n type=int,\n default=Config.max_validation_targets_per_user,\n help=\"retain this many final validation targets per user; 0 is uncapped\",\n )\n parser.add_argument(\n \"--early-stopping-patience\",\n type=int,\n default=Config.early_stopping_patience,\n )\n parser.add_argument(\n \"--early-stopping-min-delta\",\n type=float,\n default=Config.early_stopping_min_delta,\n )\n parser.add_argument(\"--checkpoint-dir\", type=Path)\n parser.add_argument(\"--resume-checkpoint\", type=Path)\n parser.add_argument(\"--seed\", type=int, default=Config.seed)\n parser.add_argument(\"--ranking-batch-size\", type=int, default=64)\n parser.add_argument(\"--prepare-only\", action=\"store_true\")\n args = parser.parse_args()\n\n if args.threads < 1 or args.bptt_steps < 2 or args.user_batch_size < 1:\n parser.error(\"threads, BPTT steps, and user batch size must be positive\")\n if (\n args.epochs < 1\n or args.learning_rate <= 0\n or args.loss_microbatch_size < 1\n or args.early_stopping_patience < 1\n or args.early_stopping_min_delta < 0\n or args.ordinary_target_weight <= 0\n or args.organic_strong_target_weight <= 0\n or args.organic_like_target_weight <= 0\n or args.discovery_horizon_days < 1\n or args.max_train_targets_per_user < 0\n or args.max_validation_targets_per_user < 0\n ):\n parser.error(\"training counts, learning rate, and patience are invalid\")\n if not 0.0 < args.validation_fraction < 1.0:\n parser.error(\"validation fraction must be strictly between zero and one\")\n public = args.public_dir.resolve()\n if not (public / \"data\" / \"candidate_catalog.parquet\").is_file():\n parser.error(f\"public task data not found under {public}\")\n config = Config(\n bptt_steps=args.bptt_steps,\n user_batch_size=args.user_batch_size,\n epochs=args.epochs,\n learning_rate=args.learning_rate,\n loss_microbatch_size=args.loss_microbatch_size,\n negative_filter_scope=args.negative_filter_scope,\n target_weighting=args.target_weighting,\n ordinary_target_weight=args.ordinary_target_weight,\n organic_strong_target_weight=args.organic_strong_target_weight,\n organic_like_target_weight=args.organic_like_target_weight,\n discovery_horizon_days=args.discovery_horizon_days,\n validation_fraction=args.validation_fraction,\n max_train_targets_per_user=args.max_train_targets_per_user,\n max_validation_targets_per_user=args.max_validation_targets_per_user,\n early_stopping_patience=args.early_stopping_patience,\n early_stopping_min_delta=args.early_stopping_min_delta,\n seed=args.seed,\n )\n torch.set_num_threads(args.threads)\n torch.set_num_interop_threads(1)\n seed_everything(config.seed)\n device = torch.device(args.device)\n if device.type == \"mps\" and not torch.backends.mps.is_available():\n parser.error(\"MPS was requested but is unavailable\")\n\n started = time.monotonic()\n prepared, eligibility = load_or_prepare(\n public,\n args.stage,\n config,\n args.prepared_cache.resolve() if args.prepared_cache else None,\n )\n target_partition = chronological_target_partition(\n prepared,\n config.validation_fraction,\n config.max_train_targets_per_user,\n config.max_validation_targets_per_user,\n )\n if args.prepare_only:\n print(\n json.dumps(\n {\n \"stage\": args.stage,\n \"sequence_users\": len(prepared.user_ids),\n \"sequence_tokens\": len(prepared.sequence_items),\n \"next_item_targets\": prepared.target_count,\n \"selected_training_targets\": int(\n target_partition.training_counts.sum()\n ),\n \"selected_validation_targets\": int(\n target_partition.validation_counts.sum()\n ),\n \"discarded_old_targets\": int(\n target_partition.window_starts.sum()\n ),\n \"target_users\": len(eligibility.uids),\n \"runtime_seconds\": round(time.monotonic() - started, 3),\n \"config\": asdict(config),\n },\n indent=2,\n )\n )\n return 0\n\n model, training_history = train_model(\n public,\n args.stage,\n prepared,\n config,\n device,\n args.checkpoint_dir.resolve() if args.checkpoint_dir else None,\n args.resume_checkpoint.resolve() if args.resume_checkpoint else None,\n )\n ranking, embedding_diagnostics = rank_candidates(\n model,\n prepared,\n eligibility,\n config,\n device,\n args.ranking_batch_size,\n )\n write_ranking(ranking, args.output.resolve())\n print(\n json.dumps(\n {\n \"baseline\": \"yambda-windowed-sequence-two-tower-base\",\n \"stage\": args.stage,\n \"output\": str(args.output.resolve()),\n \"rows\": len(ranking),\n \"users\": int(ranking.uid.nunique()),\n \"runtime_seconds\": round(time.monotonic() - started, 3),\n \"public_inputs_only\": True,\n \"architecture\": {\n \"family\": \"pure-windowed-sequence-two-tower\",\n \"sequence_training\": (\n \"every-adjacent-target-within-recent-chronological-window\"\n ),\n \"sequence_encoder\": \"one-layer-GRU-dense-timestep-loss\",\n \"bptt_hidden_state\": \"carried-and-detached\",\n \"model_selection\": \"chronological-validation-BCE-early-stopping\",\n \"loss_execution\": \"gradient-accumulated-microbatches\",\n \"user_id_embedding\": False,\n \"user_mlp\": \"582-512-256-192\",\n \"item_mlp\": \"196-512-256-192\",\n \"normalization\": \"none\",\n \"score\": \"raw-dot-product\",\n \"objective\": \"sample-weighted-BCE-at-every-timestep\",\n \"negative_sampling\": (\n \"24-uniform-plus-8-propensity-corrected-inbatch\"\n ),\n \"false_negative_filter\": config.negative_filter_scope,\n \"target_weighting\": config.target_weighting,\n },\n \"config\": asdict(config),\n \"sequence_tokens\": len(prepared.sequence_items),\n \"all_next_item_targets\": prepared.target_count,\n \"selected_training_targets\": int(\n target_partition.training_counts.sum()\n ),\n \"selected_validation_targets\": int(\n target_partition.validation_counts.sum()\n ),\n \"discarded_old_targets\": int(target_partition.window_starts.sum()),\n \"training_history\": training_history,\n \"embedding_diagnostics\": embedding_diagnostics,\n \"torch_version\": torch.__version__,\n \"device\": str(device),\n },\n indent=2,\n )\n )\n return 0\n",
"stderr": "",
"aggregated_output": " input_size=config.hidden_dim,\n hidden_size=dim,\n num_layers=1,\n batch_first=True,\n )\n self.user_tower = nn.Sequential(\n nn.Linear(dim * 3 + 6, config.user_hidden_1),\n nn.GELU(),\n nn.Dropout(config.dropout),\n nn.Linear(config.user_hidden_1, config.user_hidden_2),\n nn.GELU(),\n nn.Dropout(config.dropout),\n nn.Linear(config.user_hidden_2, dim),\n )\n self.candidate_embedding = nn.Embedding(n_items, dim)\n self.register_buffer(\n \"item_features\", torch.from_numpy(item_features), persistent=True\n )\n self.item_tower = nn.Sequential(\n nn.Linear(dim + n_item_features, config.item_hidden_1),\n nn.GELU(),\n nn.Dropout(config.dropout),\n nn.Linear(config.item_hidden_1, config.item_hidden_2),\n nn.GELU(),\n nn.Dropout(config.dropout),\n nn.Linear(config.item_hidden_2, dim),\n )\n nn.init.normal_(self.context_embedding.weight, std=0.02)\n nn.init.normal_(self.candidate_embedding.weight, std=0.02)\n with torch.no_grad():\n self.context_embedding.weight[n_items].zero_()\n\n def _mean_context_channel(self, items: torch.Tensor) -> torch.Tensor:\n mask = items.ne(self.n_items)\n vectors = self.context_embedding(items)\n return (vectors * mask.unsqueeze(-1)).sum(dim=1) / mask.sum(\n dim=1, keepdim=True\n ).clamp_min(1)\n\n def encode_items(self, items: torch.Tensor) -> torch.Tensor:\n inputs = torch.cat(\n [self.candidate_embedding(items), self.item_features[items]], dim=1\n )\n return self.item_tower(inputs)\n\n\ndef invalid_negative_mask(\n negatives: torch.Tensor,\n targets: torch.Tensor,\n context_items: torch.Tensor,\n liked_items: torch.Tensor,\n) -> torch.Tensor:\n \"\"\"Identify targets, known positives, active likes, and row duplicates.\"\"\"\n invalid = negatives.eq(targets[:, None])\n invalid |= negatives[:, :, None].eq(context_items[:, None, :]).any(dim=2)\n invalid |= negatives[:, :, None].eq(liked_items[:, None, :]).any(dim=2)\n for column in range(1, negatives.shape[1]):\n invalid[:, column] |= negatives[:, column, None].eq(\n negatives[:, :column]\n ).any(dim=1)\n return invalid\n\n\ndef draw_filtered_mixed_negatives(\n targets: torch.Tensor,\n context_items: torch.Tensor,\n liked_items: torch.Tensor,\n n_items: int,\n uniform_count: int,\n inbatch_count: int,\n item_correction: torch.Tensor,\n generator: torch.Generator,\n device: torch.device,\n) -> tuple[torch.Tensor, torch.Tensor]:\n \"\"\"Draw uniform and unique-target in-batch negatives.\"\"\"\n batch_size = len(targets)\n uniform = torch.randint(\n n_items,\n (batch_size, uniform_count),\n generator=generator,\n device=\"cpu\",\n )\n inbatch = torch.empty((batch_size, 0), dtype=torch.long, device=\"cpu\")\n corrected_count = 0\n if inbatch_count:\n unique_targets = torch.unique(targets.detach().cpu())\n corrected_count = min(inbatch_count, max(len(unique_targets) - 1, 0))\n if corrected_count:\n priorities = torch.rand(\n (batch_size, len(unique_targets)), generator=generator\n )\n priorities.masked_fill_(\n unique_targets[None, :].eq(targets.detach().cpu()[:, None]),\n -1.0,\n )\n columns = torch.topk(priorities, k=corrected_count, dim=1).indices\n inbatch = unique_targets[columns]\n if corrected_count < inbatch_count:\n filler = torch.randint(\n n_items,\n (batch_size, inbatch_count - corrected_count),\n generator=generator,\n device=\"cpu\",\n )\n inbatch = torch.cat([inbatch, filler], dim=1)\n\n negatives_cpu = torch.cat([uniform, inbatch], dim=1)\n correction_cpu = torch.ones_like(negatives_cpu, dtype=torch.float32)\n if corrected_count:\n correction_cpu[\n :, uniform_count : uniform_count + corrected_count\n ] = item_correction[inbatch[:, :corrected_count]]\n negatives = negatives_cpu.to(device)\n correction = correction_cpu.to(device)\n for _ in range(32):\n invalid = invalid_negative_mask(\n negatives, targets, context_items, liked_items\n )\n if not bool(invalid.any()):\n return negatives, correction\n replacements = torch.randint(\n n_items,\n (int(invalid.sum().detach().cpu()),),\n generator=generator,\n device=\"cpu\",\n ).to(device)\n negatives[invalid] = replacements\n correction[invalid] = 1.0\n invalid = invalid_negative_mask(\n negatives, targets, context_items, liked_items\n )\n if bool(invalid.any()):\n raise RuntimeError(\"mixed negative resampling failed false-negative filter\")\n return negatives, correction\n raw = np.power(\n (1.0 / len(counts)) / probabilities,\n config.inbatch_correction_power,\n )\n normalization = float(np.sum(counts * raw) / max(total, 1.0))\n correction = np.clip(\n raw / max(normalization, 1.0e-12),\n config.inbatch_correction_min,\n config.inbatch_correction_max,\n ).astype(np.float32)\n mean_on_targets = float(np.sum(counts * correction) / max(total, 1.0))\n log(\n f\"inbatch correction: min={correction.min():.3f} \"\n f\"mean_on_targets={mean_on_targets:.3f} max={correction.max():.3f}\"\n )\n return torch.from_numpy(correction)\n\n\ndef build_known_positive_filter(\n prepared: SequenceData, training_counts: np.ndarray\n) -> np.ndarray:\n \"\"\"Pack each user's complete public-training positive-item set.\"\"\"\n width = (len(prepared.item_ids) + 7) // 8\n packed = np.zeros((len(prepared.user_ids), width), dtype=np.uint8)\n retained = 0\n for user_row, target_count in enumerate(training_counts):\n if not target_count:\n continue\n offset = int(prepared.offsets[user_row])\n # T adjacent targets consume sequence positions 0 through T. The\n # chronological BCE suffix starts at T + 1 and is intentionally absent.\n items = np.unique(\n prepared.sequence_items[\n offset : offset + int(target_count) + 1\n ].astype(np.int64)\n )\n byte_columns = items >> 3\n bit_values = np.left_shift(\n np.uint8(1), (items & 7).astype(np.uint8)\n )\n np.bitwise_or.at(packed[user_row], byte_columns, bit_values)\n retained += len(items)\n log(\n f\"full-history false-negative filter: users={len(prepared.user_ids):,} \"\n f\"unique_user_items={retained:,} bytes={packed.nbytes:,}\"\n )\n return packed\n\n\ndef invalid_full_history_negatives(\n negatives: np.ndarray,\n targets: np.ndarray,\n user_rows: np.ndarray,\n known_positive_packed: np.ndarray,\n liked_items: np.ndarray,\n) -> np.ndarray:\n \"\"\"Return row-level invalid negatives using the complete training prefix.\"\"\"\n byte_columns = negatives >> 3\n bit_columns = negatives & 7\n known = known_positive_packed[user_rows[:, None], byte_columns]\n invalid = ((known >> bit_columns) & 1).astype(bool)\n invalid |= negatives == targets[:, None]\n invalid |= (negatives[:, :, None] == liked_items[:, None, :]).any(axis=2)\n for column in range(1, negatives.shape[1]):\n invalid[:, column] |= (\n negatives[:, column, None] == negatives[:, :column]\n ).any(axis=1)\n return invalid\n\n\ndef draw_full_history_filtered_negatives(\n targets: torch.Tensor,\n user_rows: np.ndarray,\n liked_items: torch.Tensor,\n known_positive_packed: np.ndarray,\n n_items: int,\n config: Config,\n item_correction: torch.Tensor,\n generator: torch.Generator,\n device: torch.device,\n) -> tuple[torch.Tensor, torch.Tensor]:\n \"\"\"Draw mixed negatives and reject all known public-training positives.\"\"\"\n targets_cpu = targets.detach().cpu().numpy().astype(np.int64, copy=False)\n liked_cpu = liked_items.detach().cpu().numpy().astype(np.int64, copy=False)\n batch_size = len(targets_cpu)\n uniform = torch.randint(\n n_items,\n (batch_size, config.uniform_negatives),\n generator=generator,\n device=\"cpu\",\n )\n inbatch = torch.empty((batch_size, 0), dtype=torch.long)\n corrected_count = 0\n if config.inbatch_negatives:\n unique_targets = torch.unique(targets.detach().cpu())\n corrected_count = min(\n config.inbatch_negatives, max(len(unique_targets) - 1, 0)\n )\n if corrected_count:\n priorities = torch.rand(\n (batch_size, len(unique_targets)), generator=generator\n )\n priorities.masked_fill_(\n unique_targets[None, :].eq(\n torch.from_numpy(targets_cpu)[:, None]\n ),\n -1.0,\n )\n columns = torch.topk(\n priorities, k=corrected_count, dim=1\n ).indices\n inbatch = unique_targets[columns]\n if corrected_count < config.inbatch_negatives:\n filler = torch.randint(\n n_items,\n (batch_size, config.inbatch_negatives - corrected_count),\n generator=generator,\n device=\"cpu\",\n )\n inbatch = torch.cat([inbatch, filler], dim=1)\n\n negatives = torch.cat([uniform, inbatch], dim=1).numpy()\n correction = np.ones_like(negatives, dtype=np.float32)\n if corrected_count:\n correction[\n :, config.uniform_negatives : config.uniform_negatives + corrected_count\n ] = item_correction[inbatch[:, :corrected_count]].numpy()\n for _ in range(32):\n invalid = invalid_full_history_negatives(\n negatives,\n targets_cpu,\n user_rows,\n known_positive_packed,\n liked_cpu,\n )\n if not np.any(invalid):\n return (\n torch.from_numpy(negatives).to(device),\n torch.from_numpy(correction).to(device),\n )\n replacements = torch.randint(\n n_items,\n (int(invalid.sum()),),\n generator=generator,\n device=\"cpu\",\n ).numpy()\n negatives[invalid] = replacements\n # Rejected in-batch items become ordinary uniform draws.\n correction[invalid] = 1.0\n invalid = invalid_full_history_negatives(\n negatives,\n targets_cpu,\n user_rows,\n known_positive_packed,\n liked_cpu,\n )\n if np.any(invalid):\n raise RuntimeError(\"full-history negative resampling failed\")\n return (\n torch.from_numpy(negatives).to(device),\n torch.from_numpy(correction).to(device),\n )\n\n\ndef draw_training_negatives(\n positive_items: torch.Tensor,\n known_history: torch.Tensor,\n liked_items: torch.Tensor,\n user_rows: np.ndarray,\n known_positive_packed: np.ndarray | None,\n prepared: SequenceData,\n config: Config,\n item_correction: torch.Tensor,\n generator: torch.Generator,\n device: torch.device,\n) -> tuple[torch.Tensor, torch.Tensor]:\n if config.negative_filter_scope == \"full-training-history\":\n if known_positive_packed is None:\n raise RuntimeError(\"full-history negative filter was not built\")\n return draw_full_history_filtered_negatives(\n positive_items,\n user_rows,\n liked_items,\n known_positive_packed,\n len(prepared.item_ids),\n config,\n item_correction,\n generator,\n device,\n )\n return base.draw_filtered_mixed_negatives(\n positive_items,\n known_history,\n liked_items,\n len(prepared.item_ids),\n config.uniform_negatives,\n config.inbatch_negatives,\n item_correction,\n generator,\n device,\n )\n\n\ndef event_sequence(\n model: base.SequentialTwoTower,\n items: torch.Tensor,\n features: torch.Tensor,\n hidden: torch.Tensor,\n) -> tuple[torch.Tensor, torch.Tensor]:\n inputs = torch.cat([model.context_embedding(items), features], dim=-1)\n projected = model.event_projection(inputs)\n return model.sequence_encoder(projected, hidden)\n\n\ndef pool_explicit_states(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n state_ids: np.ndarray,\n device: torch.device,\n) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:\n unique_states, inverse = np.unique(state_ids, return_inverse=True)\n sequence_vectors: torch.Tensor,\n liked_vectors: torch.Tensor,\n disliked_vectors: torch.Tensor,\n user_features: torch.Tensor,\n) -> torch.Tensor:\n return model.user_tower(\n torch.cat(\n [sequence_vectors, liked_vectors, disliked_vectors, user_features],\n dim=1,\n )\n )\n\n\ndef candidate_vectors(\n model: base.SequentialTwoTower,\n positives: torch.Tensor,\n negatives: torch.Tensor,\n) -> tuple[torch.Tensor, torch.Tensor]:\n all_items = torch.cat([positives, negatives.reshape(-1)])\n unique_items, inverse = torch.unique(all_items, return_inverse=True)\n encoded = model.encode_items(unique_items)\n positive_vectors = encoded[inverse[: len(positives)]]\n negative_vectors = encoded[inverse[len(positives) :]].reshape(\n len(positives), negatives.shape[1], -1\n )\n return positive_vectors, negative_vectors\n\n\ndef history_windows(\n sequence: np.ndarray,\n start: int,\n count: int,\n width: int,\n padding_item: int,\n) -> np.ndarray:\n base_start = max(0, start - width + 1)\n segment = sequence[base_start : start + count]\n padded = np.concatenate(\n [np.full(width - 1, padding_item, dtype=np.uint16), segment]\n )\n windows = np.lib.stride_tricks.sliding_window_view(padded, width)\n offset = start - base_start\n return windows[offset : offset + count].copy()\n\n\ndef corrected_negative_loss(\n logits: torch.Tensor,\n correction: torch.Tensor,\n config: Config,\n) -> torch.Tensor:\n raw = F.softplus(logits)\n total = config.uniform_negatives + config.inbatch_negatives\n result = torch.zeros(len(logits), device=logits.device)\n if config.uniform_negatives:\n result += config.uniform_negatives * raw[\n :, : config.uniform_negatives\n ].mean(dim=1)\n if config.inbatch_negatives:\n selected = slice(config.uniform_negatives, total)\n weights = correction[:, selected]\n result += config.inbatch_negatives * (\n (raw[:, selected] * weights).sum(dim=1)\n / weights.sum(dim=1).clamp_min(1.0e-6)\n )\n return result / total\n\n\ndef retained_target_window_starts(\n total_targets: np.ndarray,\n validation_counts: np.ndarray,\n training_counts: np.ndarray,\n) -> np.ndarray:\n \"\"\"Return the discarded prefix before the retained chronological suffix.\"\"\"\n training_region_ends = total_targets - validation_counts\n window_starts = training_region_ends - training_counts\n if (\n np.any(window_starts < 0)\n or np.any(training_counts < 0)\n or np.any(validation_counts < 0)\n ):\n raise RuntimeError(\"chronological target partition produced negative counts\")\n if not np.array_equal(\n window_starts + training_counts + validation_counts, total_targets\n ):\n raise RuntimeError(\"chronological target window is not a final contiguous suffix\")\n return window_starts\n\n\ndef chronological_target_partition(\n prepared: SequenceData,\n validation_fraction: float,\n max_train_targets_per_user: int,\n max_validation_targets_per_user: int,\n) -> TargetPartition:\n \"\"\"Select a recent contiguous window with a final out-of-time BCE suffix.\"\"\"\n totals = np.maximum(prepared.lengths - 1, 0).astype(np.int64)\n validation = np.floor(totals * validation_fraction).astype(np.int64)\n validation[(totals >= 2) & (validation == 0)] = 1\n if max_validation_targets_per_user:\n validation = np.minimum(validation, max_validation_targets_per_user)\n\n \"diagnostics\": diagnostics,\n \"model_state\": model_state,\n \"optimizer_state\": optimizer.state_dict(),\n \"generator_state\": generator.get_state(),\n },\n path,\n )\n log(f\"saved epoch checkpoint: {path}\")\n return path\n\n\ndef train_model(\n public: Path,\n stage: str,\n prepared: SequenceData,\n config: Config,\n device: torch.device,\n checkpoint_dir: Path | None,\n resume_checkpoint: Path | None,\n) -> tuple[base.SequentialTwoTower, list[dict[str, float]]]:\n partition = chronological_target_partition(\n prepared,\n config.validation_fraction,\n config.max_train_targets_per_user,\n config.max_validation_targets_per_user,\n )\n training_counts = partition.training_counts\n validation_counts = partition.validation_counts\n log(\n f\"chronological BCE window: train={int(training_counts.sum()):,} \"\n f\"validation={int(validation_counts.sum()):,} \"\n f\"selected={partition.selected_count:,} \"\n f\"discarded_old_targets={int(partition.window_starts.sum()):,} \"\n f\"all={prepared.target_count:,}\"\n )\n model = base.SequentialTwoTower(prepared.item_features, model_config(config)).to(\n device\n )\n optimizer = torch.optim.AdamW(\n model.parameters(), lr=config.learning_rate, weight_decay=config.weight_decay\n )\n generator = torch.Generator(device=\"cpu\").manual_seed(config.seed + 10_000)\n item_correction = build_item_correction(\n prepared,\n config,\n partition.window_starts,\n training_counts,\n )\n target_weights = build_target_weights(\n public,\n stage,\n prepared,\n partition.history_training_counts,\n config,\n )\n known_positive_packed = (\n build_known_positive_filter(prepared, partition.history_training_counts)\n if config.negative_filter_scope == \"full-training-history\"\n else None\n )\n history: list[dict[str, float]] = []\n best_loss = float(\"inf\")\n best_epoch = 0\n best_state: dict[str, torch.Tensor] | None = None\n epochs_without_improvement = 0\n start_epoch = 0\n\n if resume_checkpoint is not None:\n payload = torch.load(\n resume_checkpoint, map_location=device, weights_only=False\n )\n stored_config = payload.get(\"config\", {})\n architecture_fields = (\n \"bptt_steps\",\n \"embedding_dim\",\n \"hidden_dim\",\n \"user_hidden_1\",\n \"user_hidden_2\",\n \"item_hidden_1\",\n \"item_hidden_2\",\n )\n mismatches = {\n name: (stored_config.get(name), getattr(config, name))\n for name in architecture_fields\n if stored_config.get(name) != getattr(config, name)\n }\n if mismatches:\n raise RuntimeError(\n f\"resume checkpoint architecture differs: {mismatches}\"\n )\n stored_filter_scope = stored_config.get(\n \"negative_filter_scope\", \"last-100\"\n )\n stored_target_weighting = stored_config.get(\n \"target_weighting\", \"uniform\"\n )\n stored_weight_values = (\n stored_config.get(\"ordinary_target_weight\", 1.0),\n stored_config.get(\"organic_strong_target_weight\", 1.0),\n stored_config.get(\"organic_like_target_weight\", 3.0),\n stored_config.get(\"discovery_horizon_days\", 7),\n )\n current_weight_values = (\n config.ordinary_target_weight,\n config.organic_strong_target_weight,\n config.organic_like_target_weight,\n config.discovery_horizon_days,\n )\n stored_window_values = (\n stored_config.get(\"validation_fraction\", 0.10),\n stored_config.get(\"max_train_targets_per_user\", 0),\n stored_config.get(\"max_validation_targets_per_user\", 0),\n )\n current_window_values = (\n config.validation_fraction,\n config.max_train_targets_per_user,\n config.max_validation_targets_per_user,\n )\n objective_changed = (\n stored_filter_scope != config.negative_filter_scope\n or stored_target_weighting != config.target_weighting\n or stored_weight_values != current_weight_values\n or stored_window_values != current_window_values\n )\n model.load_state_dict(payload[\"model_state\"])\n if \"optimizer_state\" in payload and not objective_changed:\n optimizer.load_state_dict(payload[\"optimizer_state\"])\n for group in optimizer.param_groups:\n group[\"lr\"] = config.learning_rate\n group[\"weight_decay\"] = config.weight_decay\n log(f\"restored optimizer state from {resume_checkpoint}\")\n else:\n log(\n f\"using a fresh AdamW optimizer after loading {resume_checkpoint}\"\n )\n if \"generator_state\" in payload and not objective_changed:\n generator.set_state(payload[\"generator_state\"])\n start_epoch = int(payload[\"epoch\"])\n if start_epoch >= config.epochs:\n raise RuntimeError(\n f\"resume epoch {start_epoch} must be below --epochs {config.epochs}\"\n )\n prior = {\n str(name): float(value)\n for name, value in payload.get(\"diagnostics\", {}).items()\n }\n if objective_changed:\n prior.pop(\"validation_loss\", None)\n log(\n \"BCE objective changed: filter \"\n f\"{stored_filter_scope}->{config.negative_filter_scope}, \"\n \"target weighting \"\n f\"{stored_target_weighting}->{config.target_weighting}, \"\n f\"target window {stored_window_values}->{current_window_values}; \"\n \"recomputing the resume checkpoint's validation BCE\"\n )\n if \"validation_loss\" not in prior:\n prior.update(\n evaluate_validation_bce(\n model,\n prepared,\n config,\n target_weights,\n partition.window_starts,\n training_counts,\n validation_counts,\n item_correction,\n known_positive_packed,\n device,\n )\n )\n history.append(prior)\n best_loss = float(prior[\"validation_loss\"])\n best_epoch = start_epoch\n best_state = snapshot_model(model)\n log(\n f\"resumed validation-BCE training: epoch={start_epoch} \"\n f\"loss={best_loss:.6f}\"\n )\n\n for epoch in range(start_epoch + 1, config.epochs + 1):\n diagnostics = run_training_epoch(\n model,\n optimizer,\n prepared,\n config,\n target_weights,\n partition.window_starts,\n training_counts,\n item_correction,\n known_positive_packed,\n generator,\n device,\n epoch,\n )\n validation = evaluate_validation_bce(\n model,\n prepared,\n config,\n target_weights,\n partition.window_starts,\n training_counts,\n validation_counts,\n item_correction,\n known_positive_packed,\n device,\n )\n diagnostics.update(validation)\n history.append(diagnostics)\n current_state = snapshot_model(model)\n if checkpoint_dir is not None:\n save_epoch_checkpoint(\n checkpoint_dir,\n epoch,\n current_state,\n config,\n diagnostics,\n optimizer,\n generator,\n )\n improved = (\n diagnostics[\"validation_loss\"]\n < best_loss - config.early_stopping_min_delta\n )\n if improved:\n best_loss = diagnostics[\"validation_loss\"]\n best_epoch = epoch\n best_state = current_state\n epochs_without_improvement = 0\n if checkpoint_dir is not None:\n torch.save(\n {\n \"epoch\": epoch,\n \"config\": asdict(config),\n \"diagnostics\": diagnostics,\n \"model_state\": current_state,\n \"optimizer_state\": optimizer.state_dict(),\n \"generator_state\": generator.get_state(),\n },\n checkpoint_dir / \"best.pt\",\n )\n else:\n epochs_without_improvement += 1\n log(\n f\"epoch={epoch}/{config.epochs} train_loss={diagnostics['loss']:.5f} \"\n f\"validation_loss={diagnostics['validation_loss']:.5f} \"\n f\"pair_accuracy={diagnostics['pair_accuracy']:.3f} \"\n f\"train_targets={int(diagnostics['targets']):,} \"\n f\"validation_targets={int(diagnostics['validation_targets']):,} \"\n f\"train_seconds={diagnostics['seconds']:.1f} \"\n f\"validation_seconds={diagnostics['validation_seconds']:.1f}\"\n )\n release_accelerator_cache(device)\n if epochs_without_improvement >= config.early_stopping_patience:\n log(\n f\"early stopping after epoch={epoch}; \"\n f\"best_epoch={best_epoch} best_validation_loss={best_loss:.6f}\"\n )\n break\n\n if best_state is None:\n raise RuntimeError(\"training did not produce a valid checkpoint\")\n model.load_state_dict(best_state)\n model.to(device).eval()\n log(\n f\"restored best validation-BCE checkpoint: epoch={best_epoch} \"\n f\"loss={best_loss:.6f}\"\n )\n return model, history\n\n\[email protected]_mode()\ndef inference_queries(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n config: Config,\n device: torch.device,\n) -> torch.Tensor:\n sequence_rows = prepared.inference_user_rows\n lengths = np.where(\n sequence_rows >= 0, prepared.lengths[np.maximum(sequence_rows, 0)], 0\[email protected]_mode()\ndef rank_candidates(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n eligibility: CandidateSet,\n config: Config,\n device: torch.device,\n ranking_batch_size: int,\n) -> tuple[pd.DataFrame, dict[str, float]]:\n model.eval()\n queries = inference_queries(model, prepared, config, device)\n item_parts: list[torch.Tensor] = []\n for start in range(0, len(prepared.item_ids), 2_048):\n rows = torch.arange(\n start,\n min(start + 2_048, len(prepared.item_ids)),\n dtype=torch.long,\n device=device,\n )\n item_parts.append(model.encode_items(rows).cpu())\n items = torch.cat(item_parts)\n rows: list[tuple[int, int, int]] = []\n for start in range(0, len(eligibility.uids), ranking_batch_size):\n end = min(start + ranking_batch_size, len(eligibility.uids))\n scores = queries[start:end] @ items.T\n eligible = np.unpackbits(\n eligibility.packed[start:end],\n axis=1,\n count=len(prepared.item_ids),\n bitorder=\"little\",\n ).astype(bool, copy=False)\n scores.masked_fill_(~torch.from_numpy(eligible), -torch.inf)\n selected = torch.topk(scores, k=base.TOP_N, dim=1).indices.numpy()\n for local_row, columns in enumerate(selected):\n uid = int(eligibility.uids[start + local_row])\n rows.extend(\n (uid, int(prepared.item_ids[column]), rank)\n for rank, column in enumerate(columns, start=1)\n )\n if end == len(eligibility.uids) or end % 1_024 == 0:\n log(f\"ranked users={end:,}/{len(eligibility.uids):,}\")\n query_norm = torch.linalg.vector_norm(queries, dim=1).numpy()\n item_norm = torch.linalg.vector_norm(items, dim=1).numpy()\n diagnostics = {\n \"query_norm_mean\": float(query_norm.mean()),\n \"query_norm_std\": float(query_norm.std()),\n \"item_norm_mean\": float(item_norm.mean()),\n \"item_norm_std\": float(item_norm.std()),\n }\n return pd.DataFrame(rows, columns=[\"uid\", \"item_id\", \"rank\"]), diagnostics\n\n\ndef write_ranking(frame: pd.DataFrame, output: Path) -> None:\n output.parent.mkdir(parents=True, exist_ok=True)\n if output.suffix == \".parquet\":\n frame.to_parquet(output, index=False, compression=\"zstd\")\n elif output.suffix == \".csv\":\n frame.to_csv(output, index=False)\n else:\n raise ValueError(\"--output must end in .parquet or .csv\")\n\n\ndef main() -> int:\n parser = argparse.ArgumentParser(description=__doc__)\n parser.add_argument(\n \"--stage\", choices=(\"validation\", \"submission\"), default=\"validation\"\n )\n parser.add_argument(\"--output\", type=Path, required=True)\n parser.add_argument(\"--public-dir\", type=Path, required=True)\n parser.add_argument(\"--prepared-cache\", type=Path)\n parser.add_argument(\"--device\", choices=(\"cpu\", \"mps\"), default=\"cpu\")\n parser.add_argument(\"--threads\", type=int, default=8)\n parser.add_argument(\"--bptt-steps\", type=int, default=Config.bptt_steps)\n parser.add_argument(\n \"--user-batch-size\", type=int, default=Config.user_batch_size\n )\n parser.add_argument(\"--epochs\", type=int, default=Config.epochs)\n parser.add_argument(\"--learning-rate\", type=float, default=Config.learning_rate)\n parser.add_argument(\n \"--loss-microbatch-size\",\n type=int,\n default=Config.loss_microbatch_size,\n )\n parser.add_argument(\n \"--negative-filter-scope\",\n choices=(\"full-training-history\", \"last-100\"),\n default=Config.negative_filter_scope,\n )\n parser.add_argument(\n \"--target-weighting\",\n choices=(\"discovery-graded\", \"organic-strong\", \"uniform\"),\n default=Config.target_weighting,\n )\n parser.add_argument(\n \"--ordinary-target-weight\",\n type=float,\n default=Config.ordinary_target_weight,\n )\n parser.add_argument(\n \"--organic-strong-target-weight\",\n type=float,\n default=Config.organic_strong_target_weight,\n )\n parser.add_argument(\n \"--organic-like-target-weight\",\n type=float,\n default=Config.organic_like_target_weight,\n )\n parser.add_argument(\n \"--discovery-horizon-days\",\n type=int,\n default=Config.discovery_horizon_days,\n )\n parser.add_argument(\n \"--validation-fraction\",\n type=float,\n default=Config.validation_fraction,\n )\n parser.add_argument(\n \"--max-train-targets-per-user\",\n type=int,\n default=Config.max_train_targets_per_user,\n help=\"retain this many recent pre-validation targets per user; 0 is uncapped\",\n )\n parser.add_argument(\n \"--max-validation-targets-per-user\",\n type=int,\n default=Config.max_validation_targets_per_user,\n help=\"retain this many final validation targets per user; 0 is uncapped\",\n )\n parser.add_argument(\n \"--early-stopping-patience\",\n type=int,\n default=Config.early_stopping_patience,\n )\n parser.add_argument(\n \"--early-stopping-min-delta\",\n type=float,\n default=Config.early_stopping_min_delta,\n )\n parser.add_argument(\"--checkpoint-dir\", type=Path)\n parser.add_argument(\"--resume-checkpoint\", type=Path)\n parser.add_argument(\"--seed\", type=int, default=Config.seed)\n parser.add_argument(\"--ranking-batch-size\", type=int, default=64)\n parser.add_argument(\"--prepare-only\", action=\"store_true\")\n args = parser.parse_args()\n\n if args.threads < 1 or args.bptt_steps < 2 or args.user_batch_size < 1:\n parser.error(\"threads, BPTT steps, and user batch size must be positive\")\n if (\n args.epochs < 1\n or args.learning_rate <= 0\n or args.loss_microbatch_size < 1\n or args.early_stopping_patience < 1\n or args.early_stopping_min_delta < 0\n or args.ordinary_target_weight <= 0\n or args.organic_strong_target_weight <= 0\n or args.organic_like_target_weight <= 0\n or args.discovery_horizon_days < 1\n or args.max_train_targets_per_user < 0\n or args.max_validation_targets_per_user < 0\n ):\n parser.error(\"training counts, learning rate, and patience are invalid\")\n if not 0.0 < args.validation_fraction < 1.0:\n parser.error(\"validation fraction must be strictly between zero and one\")\n public = args.public_dir.resolve()\n if not (public / \"data\" / \"candidate_catalog.parquet\").is_file():\n parser.error(f\"public task data not found under {public}\")\n config = Config(\n bptt_steps=args.bptt_steps,\n user_batch_size=args.user_batch_size,\n epochs=args.epochs,\n learning_rate=args.learning_rate,\n loss_microbatch_size=args.loss_microbatch_size,\n negative_filter_scope=args.negative_filter_scope,\n target_weighting=args.target_weighting,\n ordinary_target_weight=args.ordinary_target_weight,\n organic_strong_target_weight=args.organic_strong_target_weight,\n organic_like_target_weight=args.organic_like_target_weight,\n discovery_horizon_days=args.discovery_horizon_days,\n validation_fraction=args.validation_fraction,\n max_train_targets_per_user=args.max_train_targets_per_user,\n max_validation_targets_per_user=args.max_validation_targets_per_user,\n early_stopping_patience=args.early_stopping_patience,\n early_stopping_min_delta=args.early_stopping_min_delta,\n seed=args.seed,\n )\n torch.set_num_threads(args.threads)\n torch.set_num_interop_threads(1)\n seed_everything(config.seed)\n device = torch.device(args.device)\n if device.type == \"mps\" and not torch.backends.mps.is_available():\n parser.error(\"MPS was requested but is unavailable\")\n\n started = time.monotonic()\n prepared, eligibility = load_or_prepare(\n public,\n args.stage,\n config,\n args.prepared_cache.resolve() if args.prepared_cache else None,\n )\n target_partition = chronological_target_partition(\n prepared,\n config.validation_fraction,\n config.max_train_targets_per_user,\n config.max_validation_targets_per_user,\n )\n if args.prepare_only:\n print(\n json.dumps(\n {\n \"stage\": args.stage,\n \"sequence_users\": len(prepared.user_ids),\n \"sequence_tokens\": len(prepared.sequence_items),\n \"next_item_targets\": prepared.target_count,\n \"selected_training_targets\": int(\n target_partition.training_counts.sum()\n ),\n \"selected_validation_targets\": int(\n target_partition.validation_counts.sum()\n ),\n \"discarded_old_targets\": int(\n target_partition.window_starts.sum()\n ),\n \"target_users\": len(eligibility.uids),\n \"runtime_seconds\": round(time.monotonic() - started, 3),\n \"config\": asdict(config),\n },\n indent=2,\n )\n )\n return 0\n\n model, training_history = train_model(\n public,\n args.stage,\n prepared,\n config,\n device,\n args.checkpoint_dir.resolve() if args.checkpoint_dir else None,\n args.resume_checkpoint.resolve() if args.resume_checkpoint else None,\n )\n ranking, embedding_diagnostics = rank_candidates(\n model,\n prepared,\n eligibility,\n config,\n device,\n args.ranking_batch_size,\n )\n write_ranking(ranking, args.output.resolve())\n print(\n json.dumps(\n {\n \"baseline\": \"yambda-windowed-sequence-two-tower-base\",\n \"stage\": args.stage,\n \"output\": str(args.output.resolve()),\n \"rows\": len(ranking),\n \"users\": int(ranking.uid.nunique()),\n \"runtime_seconds\": round(time.monotonic() - started, 3),\n \"public_inputs_only\": True,\n \"architecture\": {\n \"family\": \"pure-windowed-sequence-two-tower\",\n \"sequence_training\": (\n \"every-adjacent-target-within-recent-chronological-window\"\n ),\n \"sequence_encoder\": \"one-layer-GRU-dense-timestep-loss\",\n \"bptt_hidden_state\": \"carried-and-detached\",\n \"model_selection\": \"chronological-validation-BCE-early-stopping\",\n \"loss_execution\": \"gradient-accumulated-microbatches\",\n \"user_id_embedding\": False,\n \"user_mlp\": \"582-512-256-192\",\n \"item_mlp\": \"196-512-256-192\",\n \"normalization\": \"none\",\n \"score\": \"raw-dot-product\",\n \"objective\": \"sample-weighted-BCE-at-every-timestep\",\n \"negative_sampling\": (\n \"24-uniform-plus-8-propensity-corrected-inbatch\"\n ),\n \"false_negative_filter\": config.negative_filter_scope,\n \"target_weighting\": config.target_weighting,\n },\n \"config\": asdict(config),\n \"sequence_tokens\": len(prepared.sequence_items),\n \"all_next_item_targets\": prepared.target_count,\n \"selected_training_targets\": int(\n target_partition.training_counts.sum()\n ),\n \"selected_validation_targets\": int(\n target_partition.validation_counts.sum()\n ),\n \"discarded_old_targets\": int(target_partition.window_starts.sum()),\n \"training_history\": training_history,\n \"embedding_diagnostics\": embedding_diagnostics,\n \"torch_version\": torch.__version__,\n \"device\": str(device),\n },\n indent=2,\n )\n )\n return 0\n",
"exit_code": 0,
"duration": {
"secs": 0,
"nanos": 4250
},
"formatted_output": " input_size=config.hidden_dim,\n hidden_size=dim,\n num_layers=1,\n batch_first=True,\n )\n self.user_tower = nn.Sequential(\n nn.Linear(dim * 3 + 6, config.user_hidden_1),\n nn.GELU(),\n nn.Dropout(config.dropout),\n nn.Linear(config.user_hidden_1, config.user_hidden_2),\n nn.GELU(),\n nn.Dropout(config.dropout),\n nn.Linear(config.user_hidden_2, dim),\n )\n self.candidate_embedding = nn.Embedding(n_items, dim)\n self.register_buffer(\n \"item_features\", torch.from_numpy(item_features), persistent=True\n )\n self.item_tower = nn.Sequential(\n nn.Linear(dim + n_item_features, config.item_hidden_1),\n nn.GELU(),\n nn.Dropout(config.dropout),\n nn.Linear(config.item_hidden_1, config.item_hidden_2),\n nn.GELU(),\n nn.Dropout(config.dropout),\n nn.Linear(config.item_hidden_2, dim),\n )\n nn.init.normal_(self.context_embedding.weight, std=0.02)\n nn.init.normal_(self.candidate_embedding.weight, std=0.02)\n with torch.no_grad():\n self.context_embedding.weight[n_items].zero_()\n\n def _mean_context_channel(self, items: torch.Tensor) -> torch.Tensor:\n mask = items.ne(self.n_items)\n vectors = self.context_embedding(items)\n return (vectors * mask.unsqueeze(-1)).sum(dim=1) / mask.sum(\n dim=1, keepdim=True\n ).clamp_min(1)\n\n def encode_items(self, items: torch.Tensor) -> torch.Tensor:\n inputs = torch.cat(\n [self.candidate_embedding(items), self.item_features[items]], dim=1\n )\n return self.item_tower(inputs)\n\n\ndef invalid_negative_mask(\n negatives: torch.Tensor,\n targets: torch.Tensor,\n context_items: torch.Tensor,\n liked_items: torch.Tensor,\n) -> torch.Tensor:\n \"\"\"Identify targets, known positives, active likes, and row duplicates.\"\"\"\n invalid = negatives.eq(targets[:, None])\n invalid |= negatives[:, :, None].eq(context_items[:, None, :]).any(dim=2)\n invalid |= negatives[:, :, None].eq(liked_items[:, None, :]).any(dim=2)\n for column in range(1, negatives.shape[1]):\n invalid[:, column] |= negatives[:, column, None].eq(\n negatives[:, :column]\n ).any(dim=1)\n return invalid\n\n\ndef draw_filtered_mixed_negatives(\n targets: torch.Tensor,\n context_items: torch.Tensor,\n liked_items: torch.Tensor,\n n_items: int,\n uniform_count: int,\n inbatch_count: int,\n item_correction: torch.Tensor,\n generator: torch.Generator,\n device: torch.device,\n) -> tuple[torch.Tensor, torch.Tensor]:\n \"\"\"Draw uniform and unique-target in-batch negatives.\"\"\"\n batch_size = len(targets)\n uniform = torch.randint(\n n_items,\n (batch_size, uniform_count),\n generator=generator,\n device=\"cpu\",\n )\n inbatch = torch.empty((batch_size, 0), dtype=torch.long, device=\"cpu\")\n corrected_count = 0\n if inbatch_count:\n unique_targets = torch.unique(targets.detach().cpu())\n corrected_count = min(inbatch_count, max(len(unique_targets) - 1, 0))\n if corrected_count:\n priorities = torch.rand(\n (batch_size, len(unique_targets)), generator=generator\n )\n priorities.masked_fill_(\n unique_targets[None, :].eq(targets.detach().cpu()[:, None]),\n -1.0,\n )\n columns = torch.topk(priorities, k=corrected_count, dim=1).indices\n inbatch = unique_targets[columns]\n if corrected_count < inbatch_count:\n filler = torch.randint(\n n_items,\n (batch_size, inbatch_count - corrected_count),\n generator=generator,\n device=\"cpu\",\n )\n inbatch = torch.cat([inbatch, filler], dim=1)\n\n negatives_cpu = torch.cat([uniform, inbatch], dim=1)\n correction_cpu = torch.ones_like(negatives_cpu, dtype=torch.float32)\n if corrected_count:\n correction_cpu[\n :, uniform_count : uniform_count + corrected_count\n ] = item_correction[inbatch[:, :corrected_count]]\n negatives = negatives_cpu.to(device)\n correction = correction_cpu.to(device)\n for _ in range(32):\n invalid = invalid_negative_mask(\n negatives, targets, context_items, liked_items\n )\n if not bool(invalid.any()):\n return negatives, correction\n replacements = torch.randint(\n n_items,\n (int(invalid.sum().detach().cpu()),),\n generator=generator,\n device=\"cpu\",\n ).to(device)\n negatives[invalid] = replacements\n correction[invalid] = 1.0\n invalid = invalid_negative_mask(\n negatives, targets, context_items, liked_items\n )\n if bool(invalid.any()):\n raise RuntimeError(\"mixed negative resampling failed false-negative filter\")\n return negatives, correction\n raw = np.power(\n (1.0 / len(counts)) / probabilities,\n config.inbatch_correction_power,\n )\n normalization = float(np.sum(counts * raw) / max(total, 1.0))\n correction = np.clip(\n raw / max(normalization, 1.0e-12),\n config.inbatch_correction_min,\n config.inbatch_correction_max,\n ).astype(np.float32)\n mean_on_targets = float(np.sum(counts * correction) / max(total, 1.0))\n log(\n f\"inbatch correction: min={correction.min():.3f} \"\n f\"mean_on_targets={mean_on_targets:.3f} max={correction.max():.3f}\"\n )\n return torch.from_numpy(correction)\n\n\ndef build_known_positive_filter(\n prepared: SequenceData, training_counts: np.ndarray\n) -> np.ndarray:\n \"\"\"Pack each user's complete public-training positive-item set.\"\"\"\n width = (len(prepared.item_ids) + 7) // 8\n packed = np.zeros((len(prepared.user_ids), width), dtype=np.uint8)\n retained = 0\n for user_row, target_count in enumerate(training_counts):\n if not target_count:\n continue\n offset = int(prepared.offsets[user_row])\n # T adjacent targets consume sequence positions 0 through T. The\n # chronological BCE suffix starts at T + 1 and is intentionally absent.\n items = np.unique(\n prepared.sequence_items[\n offset : offset + int(target_count) + 1\n ].astype(np.int64)\n )\n byte_columns = items >> 3\n bit_values = np.left_shift(\n np.uint8(1), (items & 7).astype(np.uint8)\n )\n np.bitwise_or.at(packed[user_row], byte_columns, bit_values)\n retained += len(items)\n log(\n f\"full-history false-negative filter: users={len(prepared.user_ids):,} \"\n f\"unique_user_items={retained:,} bytes={packed.nbytes:,}\"\n )\n return packed\n\n\ndef invalid_full_history_negatives(\n negatives: np.ndarray,\n targets: np.ndarray,\n user_rows: np.ndarray,\n known_positive_packed: np.ndarray,\n liked_items: np.ndarray,\n) -> np.ndarray:\n \"\"\"Return row-level invalid negatives using the complete training prefix.\"\"\"\n byte_columns = negatives >> 3\n bit_columns = negatives & 7\n known = known_positive_packed[user_rows[:, None], byte_columns]\n invalid = ((known >> bit_columns) & 1).astype(bool)\n invalid |= negatives == targets[:, None]\n invalid |= (negatives[:, :, None] == liked_items[:, None, :]).any(axis=2)\n for column in range(1, negatives.shape[1]):\n invalid[:, column] |= (\n negatives[:, column, None] == negatives[:, :column]\n ).any(axis=1)\n return invalid\n\n\ndef draw_full_history_filtered_negatives(\n targets: torch.Tensor,\n user_rows: np.ndarray,\n liked_items: torch.Tensor,\n known_positive_packed: np.ndarray,\n n_items: int,\n config: Config,\n item_correction: torch.Tensor,\n generator: torch.Generator,\n device: torch.device,\n) -> tuple[torch.Tensor, torch.Tensor]:\n \"\"\"Draw mixed negatives and reject all known public-training positives.\"\"\"\n targets_cpu = targets.detach().cpu().numpy().astype(np.int64, copy=False)\n liked_cpu = liked_items.detach().cpu().numpy().astype(np.int64, copy=False)\n batch_size = len(targets_cpu)\n uniform = torch.randint(\n n_items,\n (batch_size, config.uniform_negatives),\n generator=generator,\n device=\"cpu\",\n )\n inbatch = torch.empty((batch_size, 0), dtype=torch.long)\n corrected_count = 0\n if config.inbatch_negatives:\n unique_targets = torch.unique(targets.detach().cpu())\n corrected_count = min(\n config.inbatch_negatives, max(len(unique_targets) - 1, 0)\n )\n if corrected_count:\n priorities = torch.rand(\n (batch_size, len(unique_targets)), generator=generator\n )\n priorities.masked_fill_(\n unique_targets[None, :].eq(\n torch.from_numpy(targets_cpu)[:, None]\n ),\n -1.0,\n )\n columns = torch.topk(\n priorities, k=corrected_count, dim=1\n ).indices\n inbatch = unique_targets[columns]\n if corrected_count < config.inbatch_negatives:\n filler = torch.randint(\n n_items,\n (batch_size, config.inbatch_negatives - corrected_count),\n generator=generator,\n device=\"cpu\",\n )\n inbatch = torch.cat([inbatch, filler], dim=1)\n\n negatives = torch.cat([uniform, inbatch], dim=1).numpy()\n correction = np.ones_like(negatives, dtype=np.float32)\n if corrected_count:\n correction[\n :, config.uniform_negatives : config.uniform_negatives + corrected_count\n ] = item_correction[inbatch[:, :corrected_count]].numpy()\n for _ in range(32):\n invalid = invalid_full_history_negatives(\n negatives,\n targets_cpu,\n user_rows,\n known_positive_packed,\n liked_cpu,\n )\n if not np.any(invalid):\n return (\n torch.from_numpy(negatives).to(device),\n torch.from_numpy(correction).to(device),\n )\n replacements = torch.randint(\n n_items,\n (int(invalid.sum()),),\n generator=generator,\n device=\"cpu\",\n ).numpy()\n negatives[invalid] = replacements\n # Rejected in-batch items become ordinary uniform draws.\n correction[invalid] = 1.0\n invalid = invalid_full_history_negatives(\n negatives,\n targets_cpu,\n user_rows,\n known_positive_packed,\n liked_cpu,\n )\n if np.any(invalid):\n raise RuntimeError(\"full-history negative resampling failed\")\n return (\n torch.from_numpy(negatives).to(device),\n torch.from_numpy(correction).to(device),\n )\n\n\ndef draw_training_negatives(\n positive_items: torch.Tensor,\n known_history: torch.Tensor,\n liked_items: torch.Tensor,\n user_rows: np.ndarray,\n known_positive_packed: np.ndarray | None,\n prepared: SequenceData,\n config: Config,\n item_correction: torch.Tensor,\n generator: torch.Generator,\n device: torch.device,\n) -> tuple[torch.Tensor, torch.Tensor]:\n if config.negative_filter_scope == \"full-training-history\":\n if known_positive_packed is None:\n raise RuntimeError(\"full-history negative filter was not built\")\n return draw_full_history_filtered_negatives(\n positive_items,\n user_rows,\n liked_items,\n known_positive_packed,\n len(prepared.item_ids),\n config,\n item_correction,\n generator,\n device,\n )\n return base.draw_filtered_mixed_negatives(\n positive_items,\n known_history,\n liked_items,\n len(prepared.item_ids),\n config.uniform_negatives,\n config.inbatch_negatives,\n item_correction,\n generator,\n device,\n )\n\n\ndef event_sequence(\n model: base.SequentialTwoTower,\n items: torch.Tensor,\n features: torch.Tensor,\n hidden: torch.Tensor,\n) -> tuple[torch.Tensor, torch.Tensor]:\n inputs = torch.cat([model.context_embedding(items), features], dim=-1)\n projected = model.event_projection(inputs)\n return model.sequence_encoder(projected, hidden)\n\n\ndef pool_explicit_states(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n state_ids: np.ndarray,\n device: torch.device,\n) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:\n unique_states, inverse = np.unique(state_ids, return_inverse=True)\n sequence_vectors: torch.Tensor,\n liked_vectors: torch.Tensor,\n disliked_vectors: torch.Tensor,\n user_features: torch.Tensor,\n) -> torch.Tensor:\n return model.user_tower(\n torch.cat(\n [sequence_vectors, liked_vectors, disliked_vectors, user_features],\n dim=1,\n )\n )\n\n\ndef candidate_vectors(\n model: base.SequentialTwoTower,\n positives: torch.Tensor,\n negatives: torch.Tensor,\n) -> tuple[torch.Tensor, torch.Tensor]:\n all_items = torch.cat([positives, negatives.reshape(-1)])\n unique_items, inverse = torch.unique(all_items, return_inverse=True)\n encoded = model.encode_items(unique_items)\n positive_vectors = encoded[inverse[: len(positives)]]\n negative_vectors = encoded[inverse[len(positives) :]].reshape(\n len(positives), negatives.shape[1], -1\n )\n return positive_vectors, negative_vectors\n\n\ndef history_windows(\n sequence: np.ndarray,\n start: int,\n count: int,\n width: int,\n padding_item: int,\n) -> np.ndarray:\n base_start = max(0, start - width + 1)\n segment = sequence[base_start : start + count]\n padded = np.concatenate(\n [np.full(width - 1, padding_item, dtype=np.uint16), segment]\n )\n windows = np.lib.stride_tricks.sliding_window_view(padded, width)\n offset = start - base_start\n return windows[offset : offset + count].copy()\n\n\ndef corrected_negative_loss(\n logits: torch.Tensor,\n correction: torch.Tensor,\n config: Config,\n) -> torch.Tensor:\n raw = F.softplus(logits)\n total = config.uniform_negatives + config.inbatch_negatives\n result = torch.zeros(len(logits), device=logits.device)\n if config.uniform_negatives:\n result += config.uniform_negatives * raw[\n :, : config.uniform_negatives\n ].mean(dim=1)\n if config.inbatch_negatives:\n selected = slice(config.uniform_negatives, total)\n weights = correction[:, selected]\n result += config.inbatch_negatives * (\n (raw[:, selected] * weights).sum(dim=1)\n / weights.sum(dim=1).clamp_min(1.0e-6)\n )\n return result / total\n\n\ndef retained_target_window_starts(\n total_targets: np.ndarray,\n validation_counts: np.ndarray,\n training_counts: np.ndarray,\n) -> np.ndarray:\n \"\"\"Return the discarded prefix before the retained chronological suffix.\"\"\"\n training_region_ends = total_targets - validation_counts\n window_starts = training_region_ends - training_counts\n if (\n np.any(window_starts < 0)\n or np.any(training_counts < 0)\n or np.any(validation_counts < 0)\n ):\n raise RuntimeError(\"chronological target partition produced negative counts\")\n if not np.array_equal(\n window_starts + training_counts + validation_counts, total_targets\n ):\n raise RuntimeError(\"chronological target window is not a final contiguous suffix\")\n return window_starts\n\n\ndef chronological_target_partition(\n prepared: SequenceData,\n validation_fraction: float,\n max_train_targets_per_user: int,\n max_validation_targets_per_user: int,\n) -> TargetPartition:\n \"\"\"Select a recent contiguous window with a final out-of-time BCE suffix.\"\"\"\n totals = np.maximum(prepared.lengths - 1, 0).astype(np.int64)\n validation = np.floor(totals * validation_fraction).astype(np.int64)\n validation[(totals >= 2) & (validation == 0)] = 1\n if max_validation_targets_per_user:\n validation = np.minimum(validation, max_validation_targets_per_user)\n\n \"diagnostics\": diagnostics,\n \"model_state\": model_state,\n \"optimizer_state\": optimizer.state_dict(),\n \"generator_state\": generator.get_state(),\n },\n path,\n )\n log(f\"saved epoch checkpoint: {path}\")\n return path\n\n\ndef train_model(\n public: Path,\n stage: str,\n prepared: SequenceData,\n config: Config,\n device: torch.device,\n checkpoint_dir: Path | None,\n resume_checkpoint: Path | None,\n) -> tuple[base.SequentialTwoTower, list[dict[str, float]]]:\n partition = chronological_target_partition(\n prepared,\n config.validation_fraction,\n config.max_train_targets_per_user,\n config.max_validation_targets_per_user,\n )\n training_counts = partition.training_counts\n validation_counts = partition.validation_counts\n log(\n f\"chronological BCE window: train={int(training_counts.sum()):,} \"\n f\"validation={int(validation_counts.sum()):,} \"\n f\"selected={partition.selected_count:,} \"\n f\"discarded_old_targets={int(partition.window_starts.sum()):,} \"\n f\"all={prepared.target_count:,}\"\n )\n model = base.SequentialTwoTower(prepared.item_features, model_config(config)).to(\n device\n )\n optimizer = torch.optim.AdamW(\n model.parameters(), lr=config.learning_rate, weight_decay=config.weight_decay\n )\n generator = torch.Generator(device=\"cpu\").manual_seed(config.seed + 10_000)\n item_correction = build_item_correction(\n prepared,\n config,\n partition.window_starts,\n training_counts,\n )\n target_weights = build_target_weights(\n public,\n stage,\n prepared,\n partition.history_training_counts,\n config,\n )\n known_positive_packed = (\n build_known_positive_filter(prepared, partition.history_training_counts)\n if config.negative_filter_scope == \"full-training-history\"\n else None\n )\n history: list[dict[str, float]] = []\n best_loss = float(\"inf\")\n best_epoch = 0\n best_state: dict[str, torch.Tensor] | None = None\n epochs_without_improvement = 0\n start_epoch = 0\n\n if resume_checkpoint is not None:\n payload = torch.load(\n resume_checkpoint, map_location=device, weights_only=False\n )\n stored_config = payload.get(\"config\", {})\n architecture_fields = (\n \"bptt_steps\",\n \"embedding_dim\",\n \"hidden_dim\",\n \"user_hidden_1\",\n \"user_hidden_2\",\n \"item_hidden_1\",\n \"item_hidden_2\",\n )\n mismatches = {\n name: (stored_config.get(name), getattr(config, name))\n for name in architecture_fields\n if stored_config.get(name) != getattr(config, name)\n }\n if mismatches:\n raise RuntimeError(\n f\"resume checkpoint architecture differs: {mismatches}\"\n )\n stored_filter_scope = stored_config.get(\n \"negative_filter_scope\", \"last-100\"\n )\n stored_target_weighting = stored_config.get(\n \"target_weighting\", \"uniform\"\n )\n stored_weight_values = (\n stored_config.get(\"ordinary_target_weight\", 1.0),\n stored_config.get(\"organic_strong_target_weight\", 1.0),\n stored_config.get(\"organic_like_target_weight\", 3.0),\n stored_config.get(\"discovery_horizon_days\", 7),\n )\n current_weight_values = (\n config.ordinary_target_weight,\n config.organic_strong_target_weight,\n config.organic_like_target_weight,\n config.discovery_horizon_days,\n )\n stored_window_values = (\n stored_config.get(\"validation_fraction\", 0.10),\n stored_config.get(\"max_train_targets_per_user\", 0),\n stored_config.get(\"max_validation_targets_per_user\", 0),\n )\n current_window_values = (\n config.validation_fraction,\n config.max_train_targets_per_user,\n config.max_validation_targets_per_user,\n )\n objective_changed = (\n stored_filter_scope != config.negative_filter_scope\n or stored_target_weighting != config.target_weighting\n or stored_weight_values != current_weight_values\n or stored_window_values != current_window_values\n )\n model.load_state_dict(payload[\"model_state\"])\n if \"optimizer_state\" in payload and not objective_changed:\n optimizer.load_state_dict(payload[\"optimizer_state\"])\n for group in optimizer.param_groups:\n group[\"lr\"] = config.learning_rate\n group[\"weight_decay\"] = config.weight_decay\n log(f\"restored optimizer state from {resume_checkpoint}\")\n else:\n log(\n f\"using a fresh AdamW optimizer after loading {resume_checkpoint}\"\n )\n if \"generator_state\" in payload and not objective_changed:\n generator.set_state(payload[\"generator_state\"])\n start_epoch = int(payload[\"epoch\"])\n if start_epoch >= config.epochs:\n raise RuntimeError(\n f\"resume epoch {start_epoch} must be below --epochs {config.epochs}\"\n )\n prior = {\n str(name): float(value)\n for name, value in payload.get(\"diagnostics\", {}).items()\n }\n if objective_changed:\n prior.pop(\"validation_loss\", None)\n log(\n \"BCE objective changed: filter \"\n f\"{stored_filter_scope}->{config.negative_filter_scope}, \"\n \"target weighting \"\n f\"{stored_target_weighting}->{config.target_weighting}, \"\n f\"target window {stored_window_values}->{current_window_values}; \"\n \"recomputing the resume checkpoint's validation BCE\"\n )\n if \"validation_loss\" not in prior:\n prior.update(\n evaluate_validation_bce(\n model,\n prepared,\n config,\n target_weights,\n partition.window_starts,\n training_counts,\n validation_counts,\n item_correction,\n known_positive_packed,\n device,\n )\n )\n history.append(prior)\n best_loss = float(prior[\"validation_loss\"])\n best_epoch = start_epoch\n best_state = snapshot_model(model)\n log(\n f\"resumed validation-BCE training: epoch={start_epoch} \"\n f\"loss={best_loss:.6f}\"\n )\n\n for epoch in range(start_epoch + 1, config.epochs + 1):\n diagnostics = run_training_epoch(\n model,\n optimizer,\n prepared,\n config,\n target_weights,\n partition.window_starts,\n training_counts,\n item_correction,\n known_positive_packed,\n generator,\n device,\n epoch,\n )\n validation = evaluate_validation_bce(\n model,\n prepared,\n config,\n target_weights,\n partition.window_starts,\n training_counts,\n validation_counts,\n item_correction,\n known_positive_packed,\n device,\n )\n diagnostics.update(validation)\n history.append(diagnostics)\n current_state = snapshot_model(model)\n if checkpoint_dir is not None:\n save_epoch_checkpoint(\n checkpoint_dir,\n epoch,\n current_state,\n config,\n diagnostics,\n optimizer,\n generator,\n )\n improved = (\n diagnostics[\"validation_loss\"]\n < best_loss - config.early_stopping_min_delta\n )\n if improved:\n best_loss = diagnostics[\"validation_loss\"]\n best_epoch = epoch\n best_state = current_state\n epochs_without_improvement = 0\n if checkpoint_dir is not None:\n torch.save(\n {\n \"epoch\": epoch,\n \"config\": asdict(config),\n \"diagnostics\": diagnostics,\n \"model_state\": current_state,\n \"optimizer_state\": optimizer.state_dict(),\n \"generator_state\": generator.get_state(),\n },\n checkpoint_dir / \"best.pt\",\n )\n else:\n epochs_without_improvement += 1\n log(\n f\"epoch={epoch}/{config.epochs} train_loss={diagnostics['loss']:.5f} \"\n f\"validation_loss={diagnostics['validation_loss']:.5f} \"\n f\"pair_accuracy={diagnostics['pair_accuracy']:.3f} \"\n f\"train_targets={int(diagnostics['targets']):,} \"\n f\"validation_targets={int(diagnostics['validation_targets']):,} \"\n f\"train_seconds={diagnostics['seconds']:.1f} \"\n f\"validation_seconds={diagnostics['validation_seconds']:.1f}\"\n )\n release_accelerator_cache(device)\n if epochs_without_improvement >= config.early_stopping_patience:\n log(\n f\"early stopping after epoch={epoch}; \"\n f\"best_epoch={best_epoch} best_validation_loss={best_loss:.6f}\"\n )\n break\n\n if best_state is None:\n raise RuntimeError(\"training did not produce a valid checkpoint\")\n model.load_state_dict(best_state)\n model.to(device).eval()\n log(\n f\"restored best validation-BCE checkpoint: epoch={best_epoch} \"\n f\"loss={best_loss:.6f}\"\n )\n return model, history\n\n\[email protected]_mode()\ndef inference_queries(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n config: Config,\n device: torch.device,\n) -> torch.Tensor:\n sequence_rows = prepared.inference_user_rows\n lengths = np.where(\n sequence_rows >= 0, prepared.lengths[np.maximum(sequence_rows, 0)], 0\[email protected]_mode()\ndef rank_candidates(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n eligibility: CandidateSet,\n config: Config,\n device: torch.device,\n ranking_batch_size: int,\n) -> tuple[pd.DataFrame, dict[str, float]]:\n model.eval()\n queries = inference_queries(model, prepared, config, device)\n item_parts: list[torch.Tensor] = []\n for start in range(0, len(prepared.item_ids), 2_048):\n rows = torch.arange(\n start,\n min(start + 2_048, len(prepared.item_ids)),\n dtype=torch.long,\n device=device,\n )\n item_parts.append(model.encode_items(rows).cpu())\n items = torch.cat(item_parts)\n rows: list[tuple[int, int, int]] = []\n for start in range(0, len(eligibility.uids), ranking_batch_size):\n end = min(start + ranking_batch_size, len(eligibility.uids))\n scores = queries[start:end] @ items.T\n eligible = np.unpackbits(\n eligibility.packed[start:end],\n axis=1,\n count=len(prepared.item_ids),\n bitorder=\"little\",\n ).astype(bool, copy=False)\n scores.masked_fill_(~torch.from_numpy(eligible), -torch.inf)\n selected = torch.topk(scores, k=base.TOP_N, dim=1).indices.numpy()\n for local_row, columns in enumerate(selected):\n uid = int(eligibility.uids[start + local_row])\n rows.extend(\n (uid, int(prepared.item_ids[column]), rank)\n for rank, column in enumerate(columns, start=1)\n )\n if end == len(eligibility.uids) or end % 1_024 == 0:\n log(f\"ranked users={end:,}/{len(eligibility.uids):,}\")\n query_norm = torch.linalg.vector_norm(queries, dim=1).numpy()\n item_norm = torch.linalg.vector_norm(items, dim=1).numpy()\n diagnostics = {\n \"query_norm_mean\": float(query_norm.mean()),\n \"query_norm_std\": float(query_norm.std()),\n \"item_norm_mean\": float(item_norm.mean()),\n \"item_norm_std\": float(item_norm.std()),\n }\n return pd.DataFrame(rows, columns=[\"uid\", \"item_id\", \"rank\"]), diagnostics\n\n\ndef write_ranking(frame: pd.DataFrame, output: Path) -> None:\n output.parent.mkdir(parents=True, exist_ok=True)\n if output.suffix == \".parquet\":\n frame.to_parquet(output, index=False, compression=\"zstd\")\n elif output.suffix == \".csv\":\n frame.to_csv(output, index=False)\n else:\n raise ValueError(\"--output must end in .parquet or .csv\")\n\n\ndef main() -> int:\n parser = argparse.ArgumentParser(description=__doc__)\n parser.add_argument(\n \"--stage\", choices=(\"validation\", \"submission\"), default=\"validation\"\n )\n parser.add_argument(\"--output\", type=Path, required=True)\n parser.add_argument(\"--public-dir\", type=Path, required=True)\n parser.add_argument(\"--prepared-cache\", type=Path)\n parser.add_argument(\"--device\", choices=(\"cpu\", \"mps\"), default=\"cpu\")\n parser.add_argument(\"--threads\", type=int, default=8)\n parser.add_argument(\"--bptt-steps\", type=int, default=Config.bptt_steps)\n parser.add_argument(\n \"--user-batch-size\", type=int, default=Config.user_batch_size\n )\n parser.add_argument(\"--epochs\", type=int, default=Config.epochs)\n parser.add_argument(\"--learning-rate\", type=float, default=Config.learning_rate)\n parser.add_argument(\n \"--loss-microbatch-size\",\n type=int,\n default=Config.loss_microbatch_size,\n )\n parser.add_argument(\n \"--negative-filter-scope\",\n choices=(\"full-training-history\", \"last-100\"),\n default=Config.negative_filter_scope,\n )\n parser.add_argument(\n \"--target-weighting\",\n choices=(\"discovery-graded\", \"organic-strong\", \"uniform\"),\n default=Config.target_weighting,\n )\n parser.add_argument(\n \"--ordinary-target-weight\",\n type=float,\n default=Config.ordinary_target_weight,\n )\n parser.add_argument(\n \"--organic-strong-target-weight\",\n type=float,\n default=Config.organic_strong_target_weight,\n )\n parser.add_argument(\n \"--organic-like-target-weight\",\n type=float,\n default=Config.organic_like_target_weight,\n )\n parser.add_argument(\n \"--discovery-horizon-days\",\n type=int,\n default=Config.discovery_horizon_days,\n )\n parser.add_argument(\n \"--validation-fraction\",\n type=float,\n default=Config.validation_fraction,\n )\n parser.add_argument(\n \"--max-train-targets-per-user\",\n type=int,\n default=Config.max_train_targets_per_user,\n help=\"retain this many recent pre-validation targets per user; 0 is uncapped\",\n )\n parser.add_argument(\n \"--max-validation-targets-per-user\",\n type=int,\n default=Config.max_validation_targets_per_user,\n help=\"retain this many final validation targets per user; 0 is uncapped\",\n )\n parser.add_argument(\n \"--early-stopping-patience\",\n type=int,\n default=Config.early_stopping_patience,\n )\n parser.add_argument(\n \"--early-stopping-min-delta\",\n type=float,\n default=Config.early_stopping_min_delta,\n )\n parser.add_argument(\"--checkpoint-dir\", type=Path)\n parser.add_argument(\"--resume-checkpoint\", type=Path)\n parser.add_argument(\"--seed\", type=int, default=Config.seed)\n parser.add_argument(\"--ranking-batch-size\", type=int, default=64)\n parser.add_argument(\"--prepare-only\", action=\"store_true\")\n args = parser.parse_args()\n\n if args.threads < 1 or args.bptt_steps < 2 or args.user_batch_size < 1:\n parser.error(\"threads, BPTT steps, and user batch size must be positive\")\n if (\n args.epochs < 1\n or args.learning_rate <= 0\n or args.loss_microbatch_size < 1\n or args.early_stopping_patience < 1\n or args.early_stopping_min_delta < 0\n or args.ordinary_target_weight <= 0\n or args.organic_strong_target_weight <= 0\n or args.organic_like_target_weight <= 0\n or args.discovery_horizon_days < 1\n or args.max_train_targets_per_user < 0\n or args.max_validation_targets_per_user < 0\n ):\n parser.error(\"training counts, learning rate, and patience are invalid\")\n if not 0.0 < args.validation_fraction < 1.0:\n parser.error(\"validation fraction must be strictly between zero and one\")\n public = args.public_dir.resolve()\n if not (public / \"data\" / \"candidate_catalog.parquet\").is_file():\n parser.error(f\"public task data not found under {public}\")\n config = Config(\n bptt_steps=args.bptt_steps,\n user_batch_size=args.user_batch_size,\n epochs=args.epochs,\n learning_rate=args.learning_rate,\n loss_microbatch_size=args.loss_microbatch_size,\n negative_filter_scope=args.negative_filter_scope,\n target_weighting=args.target_weighting,\n ordinary_target_weight=args.ordinary_target_weight,\n organic_strong_target_weight=args.organic_strong_target_weight,\n organic_like_target_weight=args.organic_like_target_weight,\n discovery_horizon_days=args.discovery_horizon_days,\n validation_fraction=args.validation_fraction,\n max_train_targets_per_user=args.max_train_targets_per_user,\n max_validation_targets_per_user=args.max_validation_targets_per_user,\n early_stopping_patience=args.early_stopping_patience,\n early_stopping_min_delta=args.early_stopping_min_delta,\n seed=args.seed,\n )\n torch.set_num_threads(args.threads)\n torch.set_num_interop_threads(1)\n seed_everything(config.seed)\n device = torch.device(args.device)\n if device.type == \"mps\" and not torch.backends.mps.is_available():\n parser.error(\"MPS was requested but is unavailable\")\n\n started = time.monotonic()\n prepared, eligibility = load_or_prepare(\n public,\n args.stage,\n config,\n args.prepared_cache.resolve() if args.prepared_cache else None,\n )\n target_partition = chronological_target_partition(\n prepared,\n config.validation_fraction,\n config.max_train_targets_per_user,\n config.max_validation_targets_per_user,\n )\n if args.prepare_only:\n print(\n json.dumps(\n {\n \"stage\": args.stage,\n \"sequence_users\": len(prepared.user_ids),\n \"sequence_tokens\": len(prepared.sequence_items),\n \"next_item_targets\": prepared.target_count,\n \"selected_training_targets\": int(\n target_partition.training_counts.sum()\n ),\n \"selected_validation_targets\": int(\n target_partition.validation_counts.sum()\n ),\n \"discarded_old_targets\": int(\n target_partition.window_starts.sum()\n ),\n \"target_users\": len(eligibility.uids),\n \"runtime_seconds\": round(time.monotonic() - started, 3),\n \"config\": asdict(config),\n },\n indent=2,\n )\n )\n return 0\n\n model, training_history = train_model(\n public,\n args.stage,\n prepared,\n config,\n device,\n args.checkpoint_dir.resolve() if args.checkpoint_dir else None,\n args.resume_checkpoint.resolve() if args.resume_checkpoint else None,\n )\n ranking, embedding_diagnostics = rank_candidates(\n model,\n prepared,\n eligibility,\n config,\n device,\n args.ranking_batch_size,\n )\n write_ranking(ranking, args.output.resolve())\n print(\n json.dumps(\n {\n \"baseline\": \"yambda-windowed-sequence-two-tower-base\",\n \"stage\": args.stage,\n \"output\": str(args.output.resolve()),\n \"rows\": len(ranking),\n \"users\": int(ranking.uid.nunique()),\n \"runtime_seconds\": round(time.monotonic() - started, 3),\n \"public_inputs_only\": True,\n \"architecture\": {\n \"family\": \"pure-windowed-sequence-two-tower\",\n \"sequence_training\": (\n \"every-adjacent-target-within-recent-chronological-window\"\n ),\n \"sequence_encoder\": \"one-layer-GRU-dense-timestep-loss\",\n \"bptt_hidden_state\": \"carried-and-detached\",\n \"model_selection\": \"chronological-validation-BCE-early-stopping\",\n \"loss_execution\": \"gradient-accumulated-microbatches\",\n \"user_id_embedding\": False,\n \"user_mlp\": \"582-512-256-192\",\n \"item_mlp\": \"196-512-256-192\",\n \"normalization\": \"none\",\n \"score\": \"raw-dot-product\",\n \"objective\": \"sample-weighted-BCE-at-every-timestep\",\n \"negative_sampling\": (\n \"24-uniform-plus-8-propensity-corrected-inbatch\"\n ),\n \"false_negative_filter\": config.negative_filter_scope,\n \"target_weighting\": config.target_weighting,\n },\n \"config\": asdict(config),\n \"sequence_tokens\": len(prepared.sequence_items),\n \"all_next_item_targets\": prepared.target_count,\n \"selected_training_targets\": int(\n target_partition.training_counts.sum()\n ),\n \"selected_validation_targets\": int(\n target_partition.validation_counts.sum()\n ),\n \"discarded_old_targets\": int(target_partition.window_starts.sum()),\n \"training_history\": training_history,\n \"embedding_diagnostics\": embedding_diagnostics,\n \"torch_version\": torch.__version__,\n \"device\": str(device),\n },\n indent=2,\n )\n )\n return 0\n"
}
}