Harness context codex item completed
{
"type": "codex_item_completed",
"item": {
"type": "CommandExecution",
"id": "exec-8bddd4eb-cd06-4f7b-b3e9-75165b5bf84e",
"process_id": "11414",
"command": [
"/bin/sh",
"-lc",
"pwd && ls -la /task/current_model /app/solution /task/data && cat /task/current_model/train_current_model.py"
],
"cwd": "file:///app",
"parsed_cmd": [
{
"type": "unknown",
"cmd": "pwd && ls -la /task/current_model /app/solution /task/data && cat /task/current_model/train_current_model.py"
}
],
"source": "unified_exec_startup",
"status": "completed",
"stdout": "/app\n/app/solution:\ntotal 8\ndrwxr-xr-x 2 agent agent 4096 Sep 23 05:16 .\ndrwxr-xr-x 3 agent agent 4096 Sep 23 05:16 ..\n\n/task/current_model:\ntotal 104\ndr-xr-xr-x 1 root root 4096 Sep 23 05:14 .\ndr-xr-xr-x 1 root root 4096 Sep 23 05:16 ..\n-r--r--r-- 1 root root 80058 Sep 23 05:14 train_current_model.py\n-r--r--r-- 1 root root 15985 Sep 23 05:14 two_tower_model.py\n\n/task/data:\ntotal 347392\ndr-xr-xr-x 1 root root 4096 Sep 23 05:15 .\ndr-xr-xr-x 1 root root 4096 Sep 23 05:16 ..\n-r--r--r-- 1 root root 349352 Sep 23 05:14 candidate_catalog.parquet\n-r--r--r-- 1 root root 84127 Sep 23 05:14 target_users.parquet\n-r--r--r-- 1 root root 7736341 Sep 23 05:14 test_eligible_candidates.npz\n-r--r--r-- 1 root root 1092329 Sep 23 05:15 test_preference_state.parquet\n-r--r--r-- 1 root root 857213 Sep 23 05:14 train_dislikes.parquet\n-r--r--r-- 1 root root 5374652 Sep 23 05:14 train_likes.parquet\n-r--r--r-- 1 root root 319183255 Sep 23 05:14 train_listens.parquet\n-r--r--r-- 1 root root 155988 Sep 23 05:14 train_undislikes.parquet\n-r--r--r-- 1 root root 1855885 Sep 23 05:14 train_unlikes.parquet\n-r--r--r-- 1 root root 36571 Sep 23 05:14 val_dislikes.parquet\n-r--r--r-- 1 root root 298484 Sep 23 05:14 val_likes.parquet\n-r--r--r-- 1 root root 9947609 Sep 23 05:14 val_listens.parquet\n-r--r--r-- 1 root root 7845 Sep 23 05:14 val_undislikes.parquet\n-r--r--r-- 1 root root 72536 Sep 23 05:14 val_unlikes.parquet\n-r--r--r-- 1 root root 7573821 Sep 23 05:14 validation_eligible_candidates.npz\n-r--r--r-- 1 root root 1061761 Sep 23 05:15 validation_preference_state.parquet\n#!/usr/bin/env python3\n\"\"\"Train the Yambda windowed two-tower baseline.\n\nEvery catalog listen with at least 50 percent completion is a sequence token.\nWithin each selected chronological window, every adjacent target contributes a\nloss. The fast default retains each user's most recent 128 training targets and\n16 held-out validation targets; passing zero for either cap restores the\nuncapped behavior for that side of the split. A 100-step truncated-BPTT block\ncomputes all of its timestep losses and carries the detached GRU hidden state\ninto the next block.\n\nThe model is a pure two-tower retriever: no user-ID embedding, no output\nnormalization, raw dot-product retrieval, sample-weighted binary cross entropy,\nand 24 uniform plus eight propensity-corrected unique in-batch negatives. Every\nselected adjacent target retains a nonzero weight. Only files under the supplied\npublic directory are read.\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport random\nimport sys\nimport time\nfrom dataclasses import asdict, dataclass\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch import nn\nfrom torch.nn import functional as F\n\nimport two_tower_model as base\n\n\nCACHE_VERSION = 2\n\n\n@dataclass(frozen=True)\nclass Config:\n bptt_steps: int = 100\n user_batch_size: int = 64\n embedding_dim: int = 192\n hidden_dim: int = 192\n user_hidden_1: int = 512\n user_hidden_2: int = 256\n item_hidden_1: int = 512\n item_hidden_2: int = 256\n dropout: float = 0.05\n epochs: int = 1\n learning_rate: float = 1.0e-3\n weight_decay: float = 1.0e-5\n gradient_clip: float = 2.0\n loss_microbatch_size: int = 65_536\n uniform_negatives: int = 24\n inbatch_negatives: int = 8\n inbatch_correction_power: float = 0.5\n inbatch_correction_min: float = 0.25\n inbatch_correction_max: float = 4.0\n negative_filter_scope: str = \"full-training-history\"\n target_weighting: str = \"organic-strong\"\n ordinary_target_weight: float = 0.25\n organic_strong_target_weight: float = 1.0\n organic_like_target_weight: float = 3.0\n discovery_horizon_days: int = 7\n validation_fraction: float = 0.10\n max_train_targets_per_user: int = 128\n max_validation_targets_per_user: int = 16\n early_stopping_patience: int = 1\n early_stopping_min_delta: float = 1.0e-4\n seed: int = 29\n\n\n@dataclass(frozen=True)\nclass CandidateSet:\n uids: np.ndarray\n item_ids: np.ndarray\n packed: np.ndarray\n\n @classmethod\n def read(cls, path: Path) -> \"CandidateSet\":\n with np.load(path, allow_pickle=False) as artifact:\n result = cls(\n uids=artifact[\"uids\"].astype(np.int64, copy=True),\n item_ids=artifact[\"item_ids\"].astype(np.int64, copy=True),\n packed=artifact[\"packed_eligible\"].astype(np.uint8, copy=True),\n )\n expected = (len(result.uids), (len(result.item_ids) + 7) // 8)\n if result.packed.shape != expected:\n raise ValueError(\n f\"invalid candidate artifact shape {result.packed.shape}; \"\n f\"expected {expected}\"\n )\n return result\n\n\n@dataclass(frozen=True)\nclass SequenceData:\n item_ids: np.ndarray\n item_features: np.ndarray\n user_ids: np.ndarray\n offsets: np.ndarray\n sequence_timestamps: np.ndarray\n sequence_items: np.ndarray\n event_features: np.ndarray\n activity_features: np.ndarray\n state_ids: np.ndarray\n liked_states: np.ndarray\n disliked_states: np.ndarray\n state_counts: np.ndarray\n inference_user_rows: np.ndarray\n inference_liked_items: np.ndarray\n inference_disliked_items: np.ndarray\n inference_user_features: np.ndarray\n\n @property\n def lengths(self) -> np.ndarray:\n return np.diff(self.offsets)\n\n @property\n def target_count(self) -> int:\n return int(np.maximum(self.lengths - 1, 0).sum())\n\n\n@dataclass(frozen=True)\nclass TargetPartition:\n \"\"\"A contiguous recent target window and its chronological BCE split.\"\"\"\n\n window_starts: np.ndarray\n training_counts: np.ndarray\n validation_counts: np.ndarray\n history_training_counts: np.ndarray\n\n @property\n def selected_count(self) -> int:\n return int(self.training_counts.sum() + self.validation_counts.sum())\n\n\ndef log(message: str) -> None:\n print(message, file=sys.stderr, flush=True)\n\n\ndef seed_everything(seed: int) -> None:\n random.seed(seed)\n np.random.seed(seed)\n torch.manual_seed(seed)\n\n\ndef model_config(config: Config) -> base.TowerConfig:\n \"\"\"Map the dense trainer configuration onto the unchanged tower.\"\"\"\n return base.TowerConfig(\n sequence_length=config.bptt_steps,\n embedding_dim=config.embedding_dim,\n hidden_dim=config.hidden_dim,\n user_hidden_1=config.user_hidden_1,\n user_hidden_2=config.user_hidden_2,\n item_hidden_1=config.item_hidden_1,\n item_hidden_2=config.item_hidden_2,\n dropout=config.dropout,\n epochs=config.epochs,\n learning_rate=config.learning_rate,\n weight_decay=config.weight_decay,\n gradient_clip=config.gradient_clip,\n uniform_negatives=config.uniform_negatives,\n inbatch_negatives=config.inbatch_negatives,\n inbatch_correction_power=config.inbatch_correction_power,\n inbatch_correction_min=config.inbatch_correction_min,\n inbatch_correction_max=config.inbatch_correction_max,\n seed=config.seed,\n )\n\n\ndef append_state_snapshot(\n active_likes: dict[int, int],\n active_dislikes: dict[int, int],\n n_items: int,\n liked_states: list[np.ndarray],\n disliked_states: list[np.ndarray],\n state_counts: list[tuple[int, int]],\n) -> int:\n liked_states.append(base.pack_active_items(active_likes, base.LIKE_ITEMS, n_items))\n disliked_states.append(\n base.pack_active_items(active_dislikes, base.DISLIKE_ITEMS, n_items)\n )\n state_counts.append((len(active_likes), len(active_dislikes)))\n return len(liked_states) - 1\n\n\ndef causal_activity_features(\n timestamps: np.ndarray,\n items: np.ndarray,\n organic: np.ndarray,\n completion: np.ndarray,\n) -> np.ndarray:\n \"\"\"Compute the first four reviewed user features at every event cutoff.\"\"\"\n length = len(items)\n result = np.zeros((length, 4), dtype=np.float16)\n if not length:\n return result\n left_rows = np.searchsorted(\n timestamps,\n timestamps - base.PROFILE_LOOKBACK_DAYS * base.DAY,\n side=\"left\",\n )\n rows = np.arange(length, dtype=np.int64)\n counts = rows - left_rows + 1\n organic_sum = np.cumsum(organic.astype(np.float64))\n completion_sum = np.cumsum(np.minimum(completion, 100).astype(np.float64))\n before_organic = np.where(\n left_rows > 0, organic_sum[np.maximum(left_rows - 1, 0)], 0.0\n )\n before_completion = np.where(\n left_rows > 0, completion_sum[np.maximum(left_rows - 1, 0)], 0.0\n )\n\n unique_counts = np.empty(length, dtype=np.int32)\n frequencies: dict[int, int] = {}\n left = 0\n for right, raw_item in enumerate(items):\n item = int(raw_item)\n frequencies[item] = frequencies.get(item, 0) + 1\n required_left = int(left_rows[right])\n while left < required_left:\n old_item = int(items[left])\n remaining = frequencies[old_item] - 1\n if remaining:\n frequencies[old_item] = remaining\n else:\n del frequencies[old_item]\n left += 1\n unique_counts[right] = len(frequencies)\n\n result[:, 0] = (np.log1p(counts) / 8.0).astype(np.float16)\n result[:, 1] = (np.log1p(unique_counts) / 6.0).astype(np.float16)\n result[:, 2] = (\n (organic_sum - before_organic) / np.maximum(counts, 1)\n ).astype(np.float16)\n result[:, 3] = (\n (completion_sum - before_completion)\n / np.maximum(counts, 1)\n / 100.0\n ).astype(np.float16)\n return result\n\n\ndef build_sequence_parts(\n listens: base.ListenEvents,\n explicit: base.ExplicitEvents,\n n_items: int,\n liked_states: list[np.ndarray],\n disliked_states: list[np.ndarray],\n state_counts: list[tuple[int, int]],\n) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray] | None:\n qualifying = listens.completion_pct >= base.LISTEN_POSITIVE_THRESHOLD\n timestamps = listens.timestamps[qualifying].astype(np.int64, copy=False)\n items = listens.items[qualifying].astype(np.uint16, copy=False)\n organic = listens.organic[qualifying].astype(np.float32, copy=False)\n completion = listens.completion_pct[qualifying].astype(np.float32, copy=False)\n if not len(items):\n return None\n\n event_features = np.zeros((len(items), 3), dtype=np.float16)\n event_features[:, 0] = organic.astype(np.float16)\n event_features[:, 1] = (\n np.minimum(completion, 100.0) / 100.0\n ).astype(np.float16)\n if len(items) > 1:\n gaps = np.maximum(0, timestamps[1:] - timestamps[:-1])\n event_features[1:, 2] = np.exp(\n -gaps.astype(np.float64) / (60.0 * base.DAY)\n ).astype(np.float16)\n\n activity = causal_activity_features(\n timestamps, items, organic, completion\n )\n states = np.zeros(len(items), dtype=np.uint32)\n active_likes: dict[int, int] = {}\n active_dislikes: dict[int, int] = {}\n explicit_position = 0\n current_state = 0\n for row, timestamp in enumerate(timestamps):\n changed = False\n while (\n explicit_position < len(explicit.timestamps)\n and int(explicit.timestamps[explicit_position]) < int(timestamp)\n ):\n item = int(explicit.items[explicit_position])\n event_timestamp = int(explicit.timestamps[explicit_position])\n destination = (\n active_likes\n if int(explicit.channel[explicit_position]) == 0\n else active_dislikes\n )\n if int(explicit.active[explicit_position]) == 1:\n destination[item] = event_timestamp\n else:\n destination.pop(item, None)\n explicit_position += 1\n changed = True\n if changed:\n current_state = append_state_snapshot(\n active_likes,\n active_dislikes,\n n_items,\n liked_states,\n disliked_states,\n state_counts,\n )\n states[row] = np.uint32(current_state)\n return timestamps, items.copy(), event_features, activity, states\n\n\ndef final_user_features(\n timestamps: np.ndarray,\n items: np.ndarray,\n event_features: np.ndarray,\n history_end: int,\n like_count: int,\n dislike_count: int,\n) -> np.ndarray:\n start = int(\n np.searchsorted(\n timestamps,\n history_end - base.PROFILE_LOOKBACK_DAYS * base.DAY,\n side=\"left\",\n )\n )\n result = np.zeros(6, dtype=np.float32)\n recent_items = items[start:]\n if len(recent_items):\n result[0] = np.log1p(len(recent_items)) / 8.0\n result[1] = np.log1p(len(np.unique(recent_items))) / 6.0\n result[2] = float(np.mean(event_features[start:, 0]))\n result[3] = float(np.mean(event_features[start:, 1]))\n result[4] = np.log1p(like_count) / 6.0\n result[5] = np.log1p(dislike_count) / 4.0\n return result\n\n\ndef prepare_data(\n public: Path, stage: str, config: Config\n) -> tuple[SequenceData, CandidateSet]:\n data = public / \"data\"\n catalog = pd.read_parquet(data / \"candidate_catalog.parquet\").sort_values(\n \"popularity_rank\"\n )\n item_ids = catalog.item_id.to_numpy(dtype=np.int64, copy=True)\n item_index = pd.Index(item_ids)\n n_items = len(item_ids)\n if n_items >= np.iinfo(np.uint16).max:\n raise RuntimeError(\"uint16 item storage is too small for this catalog\")\n\n if stage == \"validation\":\n prefixes = (\"train\",)\n history_end = base.TRAIN_END\n candidate_name = \"validation_eligible_candidates.npz\"\n state_name = \"validation_preference_state.parquet\"\n else:\n prefixes = (\"train\", \"val\")\n history_end = base.VALIDATION_END\n candidate_name = \"test_eligible_candidates.npz\"\n state_name = \"test_preference_state.parquet\"\n eligibility = CandidateSet.read(data / candidate_name)\n if not np.array_equal(eligibility.item_ids, item_ids):\n raise RuntimeError(\"catalog and candidate artifact item order differ\")\n\n explicit_by_user = base.load_explicit_events(data, prefixes, item_index)\n inference_likes, inference_dislikes, like_counts, dislike_counts = (\n base.load_inference_state(\n data / state_name,\n item_index,\n eligibility.uids,\n n_items,\n )\n )\n inference_rows = {int(uid): row for row, uid in enumerate(eligibility.uids)}\n inference_user_features = np.zeros((len(eligibility.uids), 6), dtype=np.float32)\n inference_sequence_rows = np.full(len(eligibility.uids), -1, dtype=np.int32)\n\n # Snapshot zero is the shared empty explicit state.\n liked_states = [np.full(base.LIKE_ITEMS, n_items, dtype=np.uint16)]\n disliked_states = [\n np.full(base.DISLIKE_ITEMS, n_items, dtype=np.uint16)\n ]\n state_counts: list[tuple[int, int]] = [(0, 0)]\n user_ids: list[int] = []\n offsets = [0]\n timestamp_parts: list[np.ndarray] = []\n item_parts: list[np.ndarray] = []\n event_parts: list[np.ndarray] = []\n activity_parts: list[np.ndarray] = []\n state_parts: list[np.ndarray] = []\n\n listen_paths = [data / f\"{prefix}_listens.parquet\" for prefix in prefixes]\n started = time.monotonic()\n for seen_users, (uid, listens) in enumerate(\n base.merge_listen_streams(listen_paths, item_index), start=1\n ):\n parts = build_sequence_parts(\n listens,\n explicit_by_user.get(uid, base.empty_explicit_events()),\n n_items,\n liked_states,\n disliked_states,\n state_counts,\n )\n if parts is None:\n continue\n sequence_row = len(user_ids)\n user_ids.append(uid)\n timestamp_parts.append(parts[0].astype(np.uint32, copy=False))\n item_parts.append(parts[1])\n event_parts.append(parts[2])\n activity_parts.append(parts[3])\n state_parts.append(parts[4])\n offsets.append(offsets[-1] + len(parts[1]))\n inference_row = inference_rows.get(uid)\n if inference_row is not None:\n inference_sequence_rows[inference_row] = sequence_row\n inference_user_features[inference_row] = final_user_features(\n parts[0],\n parts[1],\n parts[2],\n history_end,\n int(like_counts[inference_row]),\n int(dislike_counts[inference_row]),\n )\n if seen_users % 1_000 == 0:\n log(\n f\"prepared users={seen_users:,} tokens={offsets[-1]:,} \"\n f\"states={len(liked_states):,} \"\n f\"elapsed={time.monotonic() - started:.1f}s\"\n )\n\n missing = np.flatnonzero(inference_sequence_rows < 0)\n if len(missing):\n inference_user_features[missing, 4] = np.log1p(like_counts[missing]) / 6.0\n inference_user_features[missing, 5] = (\n np.log1p(dislike_counts[missing]) / 4.0\n )\n log(f\"target users without qualifying catalog sequence={len(missing):,}\")\n\n prepared = SequenceData(\n item_ids=item_ids,\n item_features=base.build_item_features(catalog),\n user_ids=np.asarray(user_ids, dtype=np.int64),\n offsets=np.asarray(offsets, dtype=np.int64),\n sequence_timestamps=np.concatenate(timestamp_parts),\n sequence_items=np.concatenate(item_parts),\n event_features=np.concatenate(event_parts),\n activity_features=np.concatenate(activity_parts),\n state_ids=np.concatenate(state_parts),\n liked_states=np.stack(liked_states),\n disliked_states=np.stack(disliked_states),\n state_counts=np.asarray(state_counts, dtype=np.int32),\n inference_user_rows=inference_sequence_rows,\n inference_liked_items=inference_likes,\n inference_disliked_items=inference_dislikes,\n inference_user_features=inference_user_features,\n )\n log(\n f\"prepared stage={stage} sequence_users={len(prepared.user_ids):,} \"\n f\"tokens={len(prepared.sequence_items):,} \"\n f\"next_item_targets={prepared.target_count:,} \"\n f\"states={len(prepared.liked_states):,}\"\n )\n return prepared, eligibility\n\n\ndef cache_metadata(stage: str, config: Config) -> dict[str, object]:\n return {\n \"cache_version\": CACHE_VERSION,\n \"stage\": stage,\n \"positive_listen_threshold\": base.LISTEN_POSITIVE_THRESHOLD,\n \"profile_lookback_days\": base.PROFILE_LOOKBACK_DAYS,\n \"event_time_feature\": \"exp-negative-gap-over-60-days\",\n }\n\n\ndef save_cache(\n path: Path, prepared: SequenceData, stage: str, config: Config\n) -> None:\n path.parent.mkdir(parents=True, exist_ok=True)\n np.savez(\n path,\n metadata=np.asarray(json.dumps(cache_metadata(stage, config), sort_keys=True)),\n **prepared.__dict__,\n )\n log(f\"saved public-derived full-sequence cache: {path}\")\n\n\ndef load_cache(path: Path, stage: str, config: Config) -> SequenceData:\n with np.load(path, allow_pickle=False) as artifact:\n actual = json.loads(str(artifact[\"metadata\"].item()))\n expected = cache_metadata(stage, config)\n if actual != expected:\n raise RuntimeError(f\"sequence cache differs: {actual} != {expected}\")\n values = {\n field: artifact[field].copy()\n for field in SequenceData.__dataclass_fields__\n }\n prepared = SequenceData(**values)\n log(\n f\"loaded full-sequence cache: tokens={len(prepared.sequence_items):,} \"\n f\"targets={prepared.target_count:,}\"\n )\n return prepared\n\n\ndef load_or_prepare(\n public: Path,\n stage: str,\n config: Config,\n cache: Path | None,\n) -> tuple[SequenceData, CandidateSet]:\n candidate_name = (\n \"validation_eligible_candidates.npz\"\n if stage == \"validation\"\n else \"test_eligible_candidates.npz\"\n )\n eligibility = CandidateSet.read(public / \"data\" / candidate_name)\n if cache is not None and cache.is_file():\n prepared = load_cache(cache, stage, config)\n if not np.array_equal(prepared.item_ids, eligibility.item_ids):\n raise RuntimeError(\"cached catalog differs from candidate artifact\")\n return prepared, eligibility\n prepared, built_eligibility = prepare_data(public, stage, config)\n if cache is not None:\n save_cache(cache, prepared, stage, config)\n return prepared, built_eligibility\n\n\ndef build_item_correction(\n prepared: SequenceData,\n config: Config,\n target_starts: np.ndarray,\n target_counts: np.ndarray,\n) -> torch.Tensor:\n \"\"\"Estimate in-batch propensities from the selected training targets.\"\"\"\n counts = np.zeros(len(prepared.item_ids), dtype=np.float64)\n for user_row, count in enumerate(target_counts):\n if not count:\n continue\n offset = int(prepared.offsets[user_row])\n target_start = int(target_starts[user_row])\n counts += np.bincount(\n prepared.sequence_items[\n offset\n + target_start\n + 1 : offset\n + target_start\n + int(count)\n + 1\n ].astype(np.int64),\n minlength=len(prepared.item_ids),\n )\n total = float(counts.sum())\n probabilities = (counts + 1.0) / (total + len(counts))\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 liked_items = torch.from_numpy(\n prepared.liked_states[unique_states].astype(np.int64)\n ).to(device)\n disliked_items = torch.from_numpy(\n prepared.disliked_states[unique_states].astype(np.int64)\n ).to(device)\n liked_vectors = model._mean_context_channel(liked_items)\n disliked_vectors = model._mean_context_channel(disliked_items)\n inverse_rows = torch.from_numpy(inverse.astype(np.int64)).to(device)\n counts = prepared.state_counts[state_ids]\n return (\n liked_vectors[inverse_rows],\n disliked_vectors[inverse_rows],\n torch.from_numpy(counts.astype(np.float32)).to(device),\n )\n\n\ndef user_queries(\n model: base.SequentialTwoTower,\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 history_training = totals - validation\n training = history_training.copy()\n if max_train_targets_per_user:\n training = np.minimum(training, max_train_targets_per_user)\n window_starts = retained_target_window_starts(totals, validation, training)\n return TargetPartition(\n window_starts=window_starts,\n training_counts=training,\n validation_counts=validation,\n history_training_counts=history_training,\n )\n\n\ndef load_organic_like_outcomes(\n public: Path, stage: str, item_ids: np.ndarray\n) -> dict[int, dict[int, np.ndarray]]:\n \"\"\"Load mapped organic-like timestamps from public fitting history.\"\"\"\n prefixes = (\"train\",) if stage == \"validation\" else (\"train\", \"val\")\n frames = [\n pd.read_parquet(\n public / \"data\" / f\"{prefix}_likes.parquet\",\n columns=[\"uid\", \"timestamp\", \"item_id\", \"is_organic\"],\n )\n for prefix in prefixes\n ]\n likes = pd.concat(frames, ignore_index=True)\n likes = likes[likes.is_organic == 1].copy()\n mapped = pd.Index(item_ids).get_indexer(\n likes.item_id.to_numpy(dtype=np.int64)\n )\n likes = likes.loc[mapped >= 0, [\"uid\", \"timestamp\"]].copy()\n likes[\"item\"] = mapped[mapped >= 0].astype(np.int32, copy=False)\n likes = likes.sort_values([\"uid\", \"item\", \"timestamp\"], kind=\"stable\")\n nested: dict[int, dict[int, list[int]]] = {}\n for raw_uid, raw_timestamp, raw_item in likes.itertuples(\n index=False, name=None\n ):\n nested.setdefault(int(raw_uid), {}).setdefault(int(raw_item), []).append(\n int(raw_timestamp)\n )\n result = {\n uid: {\n item: np.asarray(timestamps, dtype=np.int64)\n for item, timestamps in items.items()\n }\n for uid, items in nested.items()\n }\n log(\n f\"loaded organic-like outcomes: rows={len(likes):,} \"\n f\"users={len(result):,}\"\n )\n return result\n\n\ndef build_target_weights(\n public: Path,\n stage: str,\n prepared: SequenceData,\n training_counts: np.ndarray,\n config: Config,\n) -> np.ndarray:\n \"\"\"Build nonzero per-token weights without dropping adjacent targets.\"\"\"\n if config.target_weighting == \"uniform\":\n weights = np.ones(len(prepared.sequence_items), dtype=np.float32)\n else:\n weights = np.full(\n len(prepared.sequence_items),\n config.ordinary_target_weight,\n dtype=np.float32,\n )\n organic_strong = (\n (prepared.event_features[:, 0] > 0.5)\n & (prepared.event_features[:, 1] >= 0.8)\n )\n weights[organic_strong] = config.organic_strong_target_weight\n if config.target_weighting == \"discovery-graded\":\n like_outcomes = load_organic_like_outcomes(\n public, stage, prepared.item_ids\n )\n history_end = (\n base.TRAIN_END if stage == \"validation\" else base.VALIDATION_END\n )\n horizon = config.discovery_horizon_days * base.DAY\n grade_three = 0\n for user_row, uid in enumerate(prepared.user_ids):\n start = int(prepared.offsets[user_row])\n end = int(prepared.offsets[user_row + 1])\n if end - start < 2:\n continue\n last_training_target = start + int(training_counts[user_row])\n first_validation_target = last_training_target + 1\n split_timestamp = (\n int(prepared.sequence_timestamps[first_validation_target])\n if first_validation_target < end\n else history_end\n )\n user_likes = like_outcomes.get(int(uid), {})\n seen = {int(prepared.sequence_items[start])}\n for position in range(start + 1, end):\n item = int(prepared.sequence_items[position])\n novel = item not in seen\n seen.add(item)\n if not novel:\n # Repeats remain training examples, but they cannot be\n # eligible discovery outcomes at the corresponding\n # cutoff and therefore keep only the ordinary weight.\n weights[position] = config.ordinary_target_weight\n continue\n timestamps = user_likes.get(item)\n if timestamps is None:\n continue\n timestamp = int(prepared.sequence_timestamps[position])\n upper = min(\n timestamp + horizon,\n split_timestamp\n if position <= last_training_target\n else history_end,\n )\n outcome = int(np.searchsorted(timestamps, timestamp, side=\"left\"))\n if outcome < len(timestamps) and int(timestamps[outcome]) < upper:\n weights[position] = config.organic_like_target_weight\n grade_three += 1\n log(\n f\"grade-3 first-encounter targets={grade_three:,} \"\n f\"horizon_days={config.discovery_horizon_days}\"\n )\n log(\n f\"target weighting={config.target_weighting} \"\n f\"min={weights.min():.3f} mean={weights.mean():.3f} \"\n f\"max={weights.max():.3f}\"\n )\n return weights\n\n\ndef build_dense_block(\n prepared: SequenceData,\n target_weights: np.ndarray,\n users: np.ndarray,\n window_starts: np.ndarray,\n process_counts: np.ndarray,\n score_starts: np.ndarray,\n start: int,\n steps: int,\n config: Config,\n) -> tuple[np.ndarray, ...]:\n \"\"\"Build one causal sequence block and mark the target rows to score.\"\"\"\n batch_size = len(users)\n padding_item = len(prepared.item_ids)\n input_items = np.full((batch_size, steps), padding_item, dtype=np.uint16)\n event_features = np.zeros((batch_size, steps, 3), dtype=np.float16)\n activity_features = np.zeros((batch_size, steps, 4), dtype=np.float16)\n state_ids = np.zeros((batch_size, steps), dtype=np.uint32)\n targets = np.zeros((batch_size, steps), dtype=np.uint16)\n weights = np.ones((batch_size, steps), dtype=np.float32)\n scored = np.zeros((batch_size, steps), dtype=bool)\n histories = np.full(\n (batch_size, steps, config.bptt_steps),\n padding_item,\n dtype=np.uint16,\n )\n processed = np.zeros(batch_size, dtype=np.int32)\n\n lengths = prepared.lengths\n for row, user_row in enumerate(users):\n count = min(steps, max(int(process_counts[row]) - start, 0))\n if not count:\n continue\n offset = int(prepared.offsets[user_row])\n window_start = int(window_starts[row])\n absolute_start = window_start + start\n positions = slice(\n offset + absolute_start,\n offset + absolute_start + count,\n )\n target_positions = slice(\n offset + absolute_start + 1,\n offset + absolute_start + count + 1,\n )\n input_items[row, :count] = prepared.sequence_items[positions]\n event_features[row, :count] = prepared.event_features[positions]\n activity_features[row, :count] = prepared.activity_features[positions]\n state_ids[row, :count] = prepared.state_ids[positions]\n targets[row, :count] = prepared.sequence_items[target_positions]\n weights[row, :count] = target_weights[target_positions]\n score_from = min(count, max(int(score_starts[row]) - start, 0))\n scored[row, score_from:count] = True\n processed[row] = count\n user_sequence = prepared.sequence_items[\n offset : offset + int(lengths[user_row])\n ]\n histories[row, :count] = history_windows(\n user_sequence,\n absolute_start,\n count,\n config.bptt_steps,\n padding_item,\n )\n return (\n input_items,\n event_features,\n activity_features,\n state_ids,\n targets,\n scored,\n histories,\n processed,\n weights,\n )\n\n\ndef score_target_microbatches(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n config: Config,\n sequence_vectors: torch.Tensor,\n flat_states: np.ndarray,\n flat_activity: np.ndarray,\n positive_items: torch.Tensor,\n negatives: torch.Tensor,\n negative_weights: torch.Tensor,\n sample_weights: np.ndarray,\n device: torch.device,\n backward: bool,\n) -> tuple[dict[str, float], torch.Tensor | None]:\n \"\"\"Compute one logical block's exact BCE in bounded-memory slices.\"\"\"\n target_count = len(positive_items)\n total_weight = float(sample_weights.sum())\n if total_weight <= 0:\n raise RuntimeError(\"target block has no positive sample weight\")\n sequence_gradient = torch.zeros_like(sequence_vectors) if backward else None\n statistics = {\n \"loss_sum\": 0.0,\n \"positive_sum\": 0.0,\n \"negative_sum\": 0.0,\n \"pair_correct\": 0.0,\n \"weight_sum\": 0.0,\n }\n for begin in range(0, target_count, config.loss_microbatch_size):\n end = min(begin + config.loss_microbatch_size, target_count)\n selected_states = flat_states[begin:end]\n if backward:\n sequence_input = (\n sequence_vectors[begin:end].detach().requires_grad_(True)\n )\n else:\n sequence_input = sequence_vectors[begin:end]\n liked_vectors, disliked_vectors, _ = pool_explicit_states(\n model, prepared, selected_states, device\n )\n counts = prepared.state_counts[selected_states]\n numeric = np.zeros((end - begin, 6), dtype=np.float32)\n numeric[:, :4] = flat_activity[begin:end]\n numeric[:, 4] = np.log1p(counts[:, 0]) / 6.0\n numeric[:, 5] = np.log1p(counts[:, 1]) / 4.0\n query_vectors = user_queries(\n model,\n sequence_input,\n liked_vectors,\n disliked_vectors,\n torch.from_numpy(numeric).to(device),\n )\n positive_vectors, negative_vectors = candidate_vectors(\n model,\n positive_items[begin:end],\n negatives[begin:end],\n )\n positive_logits = (query_vectors * positive_vectors).sum(dim=1)\n negative_logits = torch.einsum(\n \"bd,bkd->bk\", query_vectors, negative_vectors\n )\n positive_loss = F.softplus(-positive_logits)\n negative_loss = corrected_negative_loss(\n negative_logits, negative_weights[begin:end], config\n )\n selected_weights = torch.from_numpy(\n sample_weights[begin:end].astype(np.float32, copy=False)\n ).to(device)\n loss_sum = (\n selected_weights * (positive_loss + negative_loss) / 2.0\n ).sum()\n if not bool(torch.isfinite(loss_sum)):\n raise RuntimeError(\"dense next-item training produced non-finite loss\")\n if backward:\n (loss_sum / total_weight).backward()\n if sequence_input.grad is None:\n raise RuntimeError(\"sequence microbatch did not produce a gradient\")\n sequence_gradient[begin:end] = sequence_input.grad\n statistics[\"loss_sum\"] += float(loss_sum.detach().cpu())\n statistics[\"positive_sum\"] += float(positive_logits.detach().sum().cpu())\n statistics[\"negative_sum\"] += float(negative_logits.detach().sum().cpu())\n statistics[\"pair_correct\"] += float(\n (positive_logits.detach()[:, None] > negative_logits.detach()).sum().cpu()\n )\n statistics[\"weight_sum\"] += float(selected_weights.sum().detach().cpu())\n return statistics, sequence_gradient\n\n\ndef release_accelerator_cache(device: torch.device) -> None:\n if device.type == \"mps\":\n torch.mps.empty_cache()\n\n\ndef run_training_epoch(\n model: base.SequentialTwoTower,\n optimizer: torch.optim.Optimizer,\n prepared: SequenceData,\n config: Config,\n target_weights: np.ndarray,\n window_starts: np.ndarray,\n target_counts: np.ndarray,\n item_correction: torch.Tensor,\n known_positive_packed: np.ndarray | None,\n generator: torch.Generator,\n device: torch.device,\n epoch: int,\n) -> dict[str, float]:\n model.train()\n train_users = np.flatnonzero(target_counts > 0)\n sorted_users = train_users[\n np.argsort(target_counts[train_users], kind=\"stable\")\n ]\n groups = [\n sorted_users[start : start + config.user_batch_size]\n for start in range(0, len(sorted_users), config.user_batch_size)\n ]\n group_order = torch.randperm(len(groups), generator=generator).numpy()\n negative_count = config.uniform_negatives + config.inbatch_negatives\n total_loss = 0.0\n total_targets = 0\n total_weight = 0.0\n total_positive = 0.0\n total_negative = 0.0\n total_pair_correct = 0.0\n batches = 0\n started = time.monotonic()\n\n for group_number in group_order:\n users = groups[int(group_number)]\n group_counts = target_counts[users]\n hidden = torch.zeros(\n 1, len(users), config.embedding_dim, device=device\n )\n max_targets = int(group_counts.max())\n for start in range(0, max_targets, config.bptt_steps):\n steps = min(config.bptt_steps, max_targets - start)\n block = build_dense_block(\n prepared,\n target_weights,\n users,\n window_starts[users],\n group_counts,\n np.zeros(len(users), dtype=np.int64),\n start,\n steps,\n config,\n )\n sequence_outputs, _ = event_sequence(\n model,\n torch.from_numpy(block[0].astype(np.int64)).to(device),\n torch.from_numpy(block[1].astype(np.float32)).to(device),\n hidden,\n )\n hidden = torch.stack(\n [\n sequence_outputs[row, count - 1]\n if count\n else hidden[0, row]\n for row, count in enumerate(block[7])\n ]\n ).unsqueeze(0).detach()\n flat_scored = block[5].reshape(-1)\n if not np.any(flat_scored):\n continue\n mask = torch.from_numpy(flat_scored).to(device)\n sequence_vectors = sequence_outputs.reshape(\n -1, config.embedding_dim\n )[mask]\n flat_states = block[3].reshape(-1)[flat_scored]\n positive_items = torch.from_numpy(\n block[4].reshape(-1)[flat_scored].astype(np.int64)\n ).to(device)\n known_history = torch.from_numpy(\n block[6].reshape(-1, config.bptt_steps)[flat_scored].astype(\n np.int64\n )\n ).to(device)\n liked_items = torch.from_numpy(\n prepared.liked_states[flat_states].astype(np.int64)\n ).to(device)\n flat_user_rows = np.broadcast_to(\n users[:, None], block[5].shape\n ).reshape(-1)[flat_scored]\n negatives, negative_weights = draw_training_negatives(\n positive_items,\n known_history,\n liked_items,\n flat_user_rows,\n known_positive_packed,\n prepared,\n config,\n item_correction,\n generator,\n device,\n )\n optimizer.zero_grad(set_to_none=True)\n statistics, sequence_gradient = score_target_microbatches(\n model,\n prepared,\n config,\n sequence_vectors,\n flat_states,\n block[2].reshape(-1, 4)[flat_scored].astype(np.float32),\n positive_items,\n negatives,\n negative_weights,\n block[8].reshape(-1)[flat_scored],\n device,\n backward=True,\n )\n if sequence_gradient is None:\n raise RuntimeError(\"training block omitted its sequence gradient\")\n sequence_vectors.backward(sequence_gradient)\n nn.utils.clip_grad_norm_(model.parameters(), config.gradient_clip)\n optimizer.step()\n\n count = len(positive_items)\n total_loss += statistics[\"loss_sum\"]\n total_targets += count\n total_weight += statistics[\"weight_sum\"]\n total_positive += statistics[\"positive_sum\"]\n total_negative += statistics[\"negative_sum\"]\n total_pair_correct += statistics[\"pair_correct\"]\n batches += 1\n if batches % 25 == 0:\n release_accelerator_cache(device)\n if batches % 250 == 0:\n batch_bce = statistics[\"loss_sum\"] / max(\n statistics[\"weight_sum\"], 1.0e-12\n )\n running_bce = total_loss / max(total_weight, 1.0e-12)\n log(\n f\"epoch={epoch}/{config.epochs} batches={batches:,} \"\n f\"targets={total_targets:,} \"\n f\"batch_bce={batch_bce:.6f} \"\n f\"running_bce={running_bce:.6f} \"\n f\"elapsed={time.monotonic() - started:.1f}s\"\n )\n\n return {\n \"epoch\": float(epoch),\n \"loss\": total_loss / max(total_weight, 1.0e-12),\n \"positive_logit\": total_positive / max(total_targets, 1),\n \"negative_logit\": total_negative / max(total_targets * negative_count, 1),\n \"pair_accuracy\": total_pair_correct\n / max(total_targets * negative_count, 1),\n \"targets\": float(total_targets),\n \"target_weight_sum\": total_weight,\n \"batches\": float(batches),\n \"seconds\": time.monotonic() - started,\n }\n\n\[email protected]_grad()\ndef evaluate_validation_bce(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n config: Config,\n target_weights: np.ndarray,\n window_starts: np.ndarray,\n training_counts: np.ndarray,\n validation_counts: np.ndarray,\n item_correction: torch.Tensor,\n known_positive_packed: np.ndarray | None,\n device: torch.device,\n) -> dict[str, float]:\n \"\"\"Measure deterministic BCE on each user's chronological target suffix.\"\"\"\n model.eval()\n totals = training_counts + validation_counts\n users_with_validation = np.flatnonzero(validation_counts > 0)\n ordered = users_with_validation[\n np.argsort(totals[users_with_validation], kind=\"stable\")\n ]\n groups = [\n ordered[start : start + config.user_batch_size]\n for start in range(0, len(ordered), config.user_batch_size)\n ]\n generator = torch.Generator(device=\"cpu\").manual_seed(config.seed + 20_000)\n total_loss = 0.0\n total_targets = 0\n total_weight = 0.0\n started = time.monotonic()\n\n for group_number, users in enumerate(groups, start=1):\n group_totals = totals[users]\n group_starts = training_counts[users]\n hidden = torch.zeros(\n 1, len(users), config.embedding_dim, device=device\n )\n max_targets = int(group_totals.max())\n for start in range(0, max_targets, config.bptt_steps):\n steps = min(config.bptt_steps, max_targets - start)\n block = build_dense_block(\n prepared,\n target_weights,\n users,\n window_starts[users],\n group_totals,\n group_starts,\n start,\n steps,\n config,\n )\n sequence_outputs, _ = event_sequence(\n model,\n torch.from_numpy(block[0].astype(np.int64)).to(device),\n torch.from_numpy(block[1].astype(np.float32)).to(device),\n hidden,\n )\n hidden = torch.stack(\n [\n sequence_outputs[row, count - 1]\n if count\n else hidden[0, row]\n for row, count in enumerate(block[7])\n ]\n ).unsqueeze(0)\n flat_scored = block[5].reshape(-1)\n if not np.any(flat_scored):\n continue\n mask = torch.from_numpy(flat_scored).to(device)\n sequence_vectors = sequence_outputs.reshape(\n -1, config.embedding_dim\n )[mask]\n flat_states = block[3].reshape(-1)[flat_scored]\n positive_items = torch.from_numpy(\n block[4].reshape(-1)[flat_scored].astype(np.int64)\n ).to(device)\n known_history = torch.from_numpy(\n block[6].reshape(-1, config.bptt_steps)[flat_scored].astype(\n np.int64\n )\n ).to(device)\n liked_items = torch.from_numpy(\n prepared.liked_states[flat_states].astype(np.int64)\n ).to(device)\n flat_user_rows = np.broadcast_to(\n users[:, None], block[5].shape\n ).reshape(-1)[flat_scored]\n negatives, negative_weights = draw_training_negatives(\n positive_items,\n known_history,\n liked_items,\n flat_user_rows,\n known_positive_packed,\n prepared,\n config,\n item_correction,\n generator,\n device,\n )\n statistics, _ = score_target_microbatches(\n model,\n prepared,\n config,\n sequence_vectors,\n flat_states,\n block[2].reshape(-1, 4)[flat_scored].astype(np.float32),\n positive_items,\n negatives,\n negative_weights,\n block[8].reshape(-1)[flat_scored],\n device,\n backward=False,\n )\n total_loss += statistics[\"loss_sum\"]\n total_targets += len(positive_items)\n total_weight += statistics[\"weight_sum\"]\n if group_number % 25 == 0:\n release_accelerator_cache(device)\n return {\n \"validation_loss\": total_loss / max(total_weight, 1.0e-12),\n \"validation_targets\": float(total_targets),\n \"validation_target_weight_sum\": total_weight,\n \"validation_seconds\": time.monotonic() - started,\n }\n\n\ndef snapshot_model(model: nn.Module) -> dict[str, torch.Tensor]:\n return {\n name: value.detach().cpu().clone()\n for name, value in model.state_dict().items()\n }\n\n\ndef save_epoch_checkpoint(\n checkpoint_dir: Path,\n epoch: int,\n model_state: dict[str, torch.Tensor],\n config: Config,\n diagnostics: dict[str, float],\n optimizer: torch.optim.Optimizer,\n generator: torch.Generator,\n) -> Path:\n checkpoint_dir.mkdir(parents=True, exist_ok=True)\n path = checkpoint_dir / f\"epoch-{epoch:02d}.pt\"\n torch.save(\n {\n \"epoch\": epoch,\n \"config\": asdict(config),\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\n )\n ordered = np.argsort(lengths, kind=\"stable\")\n result = torch.zeros(\n len(sequence_rows), config.embedding_dim, dtype=torch.float32\n )\n for group_start in range(0, len(ordered), config.user_batch_size):\n output_rows = ordered[group_start : group_start + config.user_batch_size]\n rows = sequence_rows[output_rows]\n group_lengths = lengths[output_rows]\n hidden = torch.zeros(\n 1, len(output_rows), config.embedding_dim, device=device\n )\n for start in range(0, int(group_lengths.max(initial=0)), config.bptt_steps):\n steps = min(\n config.bptt_steps,\n int(group_lengths.max(initial=0)) - start,\n )\n items = np.full(\n (len(output_rows), steps),\n len(prepared.item_ids),\n dtype=np.uint16,\n )\n features = np.zeros((len(output_rows), steps, 3), dtype=np.float16)\n counts = np.zeros(len(output_rows), dtype=np.int32)\n for local_row, sequence_row in enumerate(rows):\n if sequence_row < 0:\n continue\n count = min(steps, max(int(group_lengths[local_row]) - start, 0))\n if not count:\n continue\n offset = int(prepared.offsets[sequence_row])\n selected = slice(offset + start, offset + start + count)\n items[local_row, :count] = prepared.sequence_items[selected]\n features[local_row, :count] = prepared.event_features[selected]\n counts[local_row] = count\n outputs, _ = event_sequence(\n model,\n torch.from_numpy(items.astype(np.int64)).to(device),\n torch.from_numpy(features.astype(np.float32)).to(device),\n hidden,\n )\n next_rows: list[torch.Tensor] = []\n for local_row, count in enumerate(counts):\n next_rows.append(\n outputs[local_row, count - 1]\n if count\n else hidden[0, local_row]\n )\n hidden = torch.stack(next_rows).unsqueeze(0)\n\n liked_items = torch.from_numpy(\n prepared.inference_liked_items[output_rows].astype(np.int64)\n ).to(device)\n disliked_items = torch.from_numpy(\n prepared.inference_disliked_items[output_rows].astype(np.int64)\n ).to(device)\n queries = user_queries(\n model,\n hidden.squeeze(0),\n model._mean_context_channel(liked_items),\n model._mean_context_channel(disliked_items),\n torch.from_numpy(\n prepared.inference_user_features[output_rows].astype(np.float32)\n ).to(device),\n )\n result[torch.from_numpy(output_rows.astype(np.int64))] = queries.cpu()\n return result\n\n\[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\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n",
"stderr": "",
"aggregated_output": "/app\n/app/solution:\ntotal 8\ndrwxr-xr-x 2 agent agent 4096 Sep 23 05:16 .\ndrwxr-xr-x 3 agent agent 4096 Sep 23 05:16 ..\n\n/task/current_model:\ntotal 104\ndr-xr-xr-x 1 root root 4096 Sep 23 05:14 .\ndr-xr-xr-x 1 root root 4096 Sep 23 05:16 ..\n-r--r--r-- 1 root root 80058 Sep 23 05:14 train_current_model.py\n-r--r--r-- 1 root root 15985 Sep 23 05:14 two_tower_model.py\n\n/task/data:\ntotal 347392\ndr-xr-xr-x 1 root root 4096 Sep 23 05:15 .\ndr-xr-xr-x 1 root root 4096 Sep 23 05:16 ..\n-r--r--r-- 1 root root 349352 Sep 23 05:14 candidate_catalog.parquet\n-r--r--r-- 1 root root 84127 Sep 23 05:14 target_users.parquet\n-r--r--r-- 1 root root 7736341 Sep 23 05:14 test_eligible_candidates.npz\n-r--r--r-- 1 root root 1092329 Sep 23 05:15 test_preference_state.parquet\n-r--r--r-- 1 root root 857213 Sep 23 05:14 train_dislikes.parquet\n-r--r--r-- 1 root root 5374652 Sep 23 05:14 train_likes.parquet\n-r--r--r-- 1 root root 319183255 Sep 23 05:14 train_listens.parquet\n-r--r--r-- 1 root root 155988 Sep 23 05:14 train_undislikes.parquet\n-r--r--r-- 1 root root 1855885 Sep 23 05:14 train_unlikes.parquet\n-r--r--r-- 1 root root 36571 Sep 23 05:14 val_dislikes.parquet\n-r--r--r-- 1 root root 298484 Sep 23 05:14 val_likes.parquet\n-r--r--r-- 1 root root 9947609 Sep 23 05:14 val_listens.parquet\n-r--r--r-- 1 root root 7845 Sep 23 05:14 val_undislikes.parquet\n-r--r--r-- 1 root root 72536 Sep 23 05:14 val_unlikes.parquet\n-r--r--r-- 1 root root 7573821 Sep 23 05:14 validation_eligible_candidates.npz\n-r--r--r-- 1 root root 1061761 Sep 23 05:15 validation_preference_state.parquet\n#!/usr/bin/env python3\n\"\"\"Train the Yambda windowed two-tower baseline.\n\nEvery catalog listen with at least 50 percent completion is a sequence token.\nWithin each selected chronological window, every adjacent target contributes a\nloss. The fast default retains each user's most recent 128 training targets and\n16 held-out validation targets; passing zero for either cap restores the\nuncapped behavior for that side of the split. A 100-step truncated-BPTT block\ncomputes all of its timestep losses and carries the detached GRU hidden state\ninto the next block.\n\nThe model is a pure two-tower retriever: no user-ID embedding, no output\nnormalization, raw dot-product retrieval, sample-weighted binary cross entropy,\nand 24 uniform plus eight propensity-corrected unique in-batch negatives. Every\nselected adjacent target retains a nonzero weight. Only files under the supplied\npublic directory are read.\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport random\nimport sys\nimport time\nfrom dataclasses import asdict, dataclass\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch import nn\nfrom torch.nn import functional as F\n\nimport two_tower_model as base\n\n\nCACHE_VERSION = 2\n\n\n@dataclass(frozen=True)\nclass Config:\n bptt_steps: int = 100\n user_batch_size: int = 64\n embedding_dim: int = 192\n hidden_dim: int = 192\n user_hidden_1: int = 512\n user_hidden_2: int = 256\n item_hidden_1: int = 512\n item_hidden_2: int = 256\n dropout: float = 0.05\n epochs: int = 1\n learning_rate: float = 1.0e-3\n weight_decay: float = 1.0e-5\n gradient_clip: float = 2.0\n loss_microbatch_size: int = 65_536\n uniform_negatives: int = 24\n inbatch_negatives: int = 8\n inbatch_correction_power: float = 0.5\n inbatch_correction_min: float = 0.25\n inbatch_correction_max: float = 4.0\n negative_filter_scope: str = \"full-training-history\"\n target_weighting: str = \"organic-strong\"\n ordinary_target_weight: float = 0.25\n organic_strong_target_weight: float = 1.0\n organic_like_target_weight: float = 3.0\n discovery_horizon_days: int = 7\n validation_fraction: float = 0.10\n max_train_targets_per_user: int = 128\n max_validation_targets_per_user: int = 16\n early_stopping_patience: int = 1\n early_stopping_min_delta: float = 1.0e-4\n seed: int = 29\n\n\n@dataclass(frozen=True)\nclass CandidateSet:\n uids: np.ndarray\n item_ids: np.ndarray\n packed: np.ndarray\n\n @classmethod\n def read(cls, path: Path) -> \"CandidateSet\":\n with np.load(path, allow_pickle=False) as artifact:\n result = cls(\n uids=artifact[\"uids\"].astype(np.int64, copy=True),\n item_ids=artifact[\"item_ids\"].astype(np.int64, copy=True),\n packed=artifact[\"packed_eligible\"].astype(np.uint8, copy=True),\n )\n expected = (len(result.uids), (len(result.item_ids) + 7) // 8)\n if result.packed.shape != expected:\n raise ValueError(\n f\"invalid candidate artifact shape {result.packed.shape}; \"\n f\"expected {expected}\"\n )\n return result\n\n\n@dataclass(frozen=True)\nclass SequenceData:\n item_ids: np.ndarray\n item_features: np.ndarray\n user_ids: np.ndarray\n offsets: np.ndarray\n sequence_timestamps: np.ndarray\n sequence_items: np.ndarray\n event_features: np.ndarray\n activity_features: np.ndarray\n state_ids: np.ndarray\n liked_states: np.ndarray\n disliked_states: np.ndarray\n state_counts: np.ndarray\n inference_user_rows: np.ndarray\n inference_liked_items: np.ndarray\n inference_disliked_items: np.ndarray\n inference_user_features: np.ndarray\n\n @property\n def lengths(self) -> np.ndarray:\n return np.diff(self.offsets)\n\n @property\n def target_count(self) -> int:\n return int(np.maximum(self.lengths - 1, 0).sum())\n\n\n@dataclass(frozen=True)\nclass TargetPartition:\n \"\"\"A contiguous recent target window and its chronological BCE split.\"\"\"\n\n window_starts: np.ndarray\n training_counts: np.ndarray\n validation_counts: np.ndarray\n history_training_counts: np.ndarray\n\n @property\n def selected_count(self) -> int:\n return int(self.training_counts.sum() + self.validation_counts.sum())\n\n\ndef log(message: str) -> None:\n print(message, file=sys.stderr, flush=True)\n\n\ndef seed_everything(seed: int) -> None:\n random.seed(seed)\n np.random.seed(seed)\n torch.manual_seed(seed)\n\n\ndef model_config(config: Config) -> base.TowerConfig:\n \"\"\"Map the dense trainer configuration onto the unchanged tower.\"\"\"\n return base.TowerConfig(\n sequence_length=config.bptt_steps,\n embedding_dim=config.embedding_dim,\n hidden_dim=config.hidden_dim,\n user_hidden_1=config.user_hidden_1,\n user_hidden_2=config.user_hidden_2,\n item_hidden_1=config.item_hidden_1,\n item_hidden_2=config.item_hidden_2,\n dropout=config.dropout,\n epochs=config.epochs,\n learning_rate=config.learning_rate,\n weight_decay=config.weight_decay,\n gradient_clip=config.gradient_clip,\n uniform_negatives=config.uniform_negatives,\n inbatch_negatives=config.inbatch_negatives,\n inbatch_correction_power=config.inbatch_correction_power,\n inbatch_correction_min=config.inbatch_correction_min,\n inbatch_correction_max=config.inbatch_correction_max,\n seed=config.seed,\n )\n\n\ndef append_state_snapshot(\n active_likes: dict[int, int],\n active_dislikes: dict[int, int],\n n_items: int,\n liked_states: list[np.ndarray],\n disliked_states: list[np.ndarray],\n state_counts: list[tuple[int, int]],\n) -> int:\n liked_states.append(base.pack_active_items(active_likes, base.LIKE_ITEMS, n_items))\n disliked_states.append(\n base.pack_active_items(active_dislikes, base.DISLIKE_ITEMS, n_items)\n )\n state_counts.append((len(active_likes), len(active_dislikes)))\n return len(liked_states) - 1\n\n\ndef causal_activity_features(\n timestamps: np.ndarray,\n items: np.ndarray,\n organic: np.ndarray,\n completion: np.ndarray,\n) -> np.ndarray:\n \"\"\"Compute the first four reviewed user features at every event cutoff.\"\"\"\n length = len(items)\n result = np.zeros((length, 4), dtype=np.float16)\n if not length:\n return result\n left_rows = np.searchsorted(\n timestamps,\n timestamps - base.PROFILE_LOOKBACK_DAYS * base.DAY,\n side=\"left\",\n )\n rows = np.arange(length, dtype=np.int64)\n counts = rows - left_rows + 1\n organic_sum = np.cumsum(organic.astype(np.float64))\n completion_sum = np.cumsum(np.minimum(completion, 100).astype(np.float64))\n before_organic = np.where(\n left_rows > 0, organic_sum[np.maximum(left_rows - 1, 0)], 0.0\n )\n before_completion = np.where(\n left_rows > 0, completion_sum[np.maximum(left_rows - 1, 0)], 0.0\n )\n\n unique_counts = np.empty(length, dtype=np.int32)\n frequencies: dict[int, int] = {}\n left = 0\n for right, raw_item in enumerate(items):\n item = int(raw_item)\n frequencies[item] = frequencies.get(item, 0) + 1\n required_left = int(left_rows[right])\n while left < required_left:\n old_item = int(items[left])\n remaining = frequencies[old_item] - 1\n if remaining:\n frequencies[old_item] = remaining\n else:\n del frequencies[old_item]\n left += 1\n unique_counts[right] = len(frequencies)\n\n result[:, 0] = (np.log1p(counts) / 8.0).astype(np.float16)\n result[:, 1] = (np.log1p(unique_counts) / 6.0).astype(np.float16)\n result[:, 2] = (\n (organic_sum - before_organic) / np.maximum(counts, 1)\n ).astype(np.float16)\n result[:, 3] = (\n (completion_sum - before_completion)\n / np.maximum(counts, 1)\n / 100.0\n ).astype(np.float16)\n return result\n\n\ndef build_sequence_parts(\n listens: base.ListenEvents,\n explicit: base.ExplicitEvents,\n n_items: int,\n liked_states: list[np.ndarray],\n disliked_states: list[np.ndarray],\n state_counts: list[tuple[int, int]],\n) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray] | None:\n qualifying = listens.completion_pct >= base.LISTEN_POSITIVE_THRESHOLD\n timestamps = listens.timestamps[qualifying].astype(np.int64, copy=False)\n items = listens.items[qualifying].astype(np.uint16, copy=False)\n organic = listens.organic[qualifying].astype(np.float32, copy=False)\n completion = listens.completion_pct[qualifying].astype(np.float32, copy=False)\n if not len(items):\n return None\n\n event_features = np.zeros((len(items), 3), dtype=np.float16)\n event_features[:, 0] = organic.astype(np.float16)\n event_features[:, 1] = (\n np.minimum(completion, 100.0) / 100.0\n ).astype(np.float16)\n if len(items) > 1:\n gaps = np.maximum(0, timestamps[1:] - timestamps[:-1])\n event_features[1:, 2] = np.exp(\n -gaps.astype(np.float64) / (60.0 * base.DAY)\n ).astype(np.float16)\n\n activity = causal_activity_features(\n timestamps, items, organic, completion\n )\n states = np.zeros(len(items), dtype=np.uint32)\n active_likes: dict[int, int] = {}\n active_dislikes: dict[int, int] = {}\n explicit_position = 0\n current_state = 0\n for row, timestamp in enumerate(timestamps):\n changed = False\n while (\n explicit_position < len(explicit.timestamps)\n and int(explicit.timestamps[explicit_position]) < int(timestamp)\n ):\n item = int(explicit.items[explicit_position])\n event_timestamp = int(explicit.timestamps[explicit_position])\n destination = (\n active_likes\n if int(explicit.channel[explicit_position]) == 0\n else active_dislikes\n )\n if int(explicit.active[explicit_position]) == 1:\n destination[item] = event_timestamp\n else:\n destination.pop(item, None)\n explicit_position += 1\n changed = True\n if changed:\n current_state = append_state_snapshot(\n active_likes,\n active_dislikes,\n n_items,\n liked_states,\n disliked_states,\n state_counts,\n )\n states[row] = np.uint32(current_state)\n return timestamps, items.copy(), event_features, activity, states\n\n\ndef final_user_features(\n timestamps: np.ndarray,\n items: np.ndarray,\n event_features: np.ndarray,\n history_end: int,\n like_count: int,\n dislike_count: int,\n) -> np.ndarray:\n start = int(\n np.searchsorted(\n timestamps,\n history_end - base.PROFILE_LOOKBACK_DAYS * base.DAY,\n side=\"left\",\n )\n )\n result = np.zeros(6, dtype=np.float32)\n recent_items = items[start:]\n if len(recent_items):\n result[0] = np.log1p(len(recent_items)) / 8.0\n result[1] = np.log1p(len(np.unique(recent_items))) / 6.0\n result[2] = float(np.mean(event_features[start:, 0]))\n result[3] = float(np.mean(event_features[start:, 1]))\n result[4] = np.log1p(like_count) / 6.0\n result[5] = np.log1p(dislike_count) / 4.0\n return result\n\n\ndef prepare_data(\n public: Path, stage: str, config: Config\n) -> tuple[SequenceData, CandidateSet]:\n data = public / \"data\"\n catalog = pd.read_parquet(data / \"candidate_catalog.parquet\").sort_values(\n \"popularity_rank\"\n )\n item_ids = catalog.item_id.to_numpy(dtype=np.int64, copy=True)\n item_index = pd.Index(item_ids)\n n_items = len(item_ids)\n if n_items >= np.iinfo(np.uint16).max:\n raise RuntimeError(\"uint16 item storage is too small for this catalog\")\n\n if stage == \"validation\":\n prefixes = (\"train\",)\n history_end = base.TRAIN_END\n candidate_name = \"validation_eligible_candidates.npz\"\n state_name = \"validation_preference_state.parquet\"\n else:\n prefixes = (\"train\", \"val\")\n history_end = base.VALIDATION_END\n candidate_name = \"test_eligible_candidates.npz\"\n state_name = \"test_preference_state.parquet\"\n eligibility = CandidateSet.read(data / candidate_name)\n if not np.array_equal(eligibility.item_ids, item_ids):\n raise RuntimeError(\"catalog and candidate artifact item order differ\")\n\n explicit_by_user = base.load_explicit_events(data, prefixes, item_index)\n inference_likes, inference_dislikes, like_counts, dislike_counts = (\n base.load_inference_state(\n data / state_name,\n item_index,\n eligibility.uids,\n n_items,\n )\n )\n inference_rows = {int(uid): row for row, uid in enumerate(eligibility.uids)}\n inference_user_features = np.zeros((len(eligibility.uids), 6), dtype=np.float32)\n inference_sequence_rows = np.full(len(eligibility.uids), -1, dtype=np.int32)\n\n # Snapshot zero is the shared empty explicit state.\n liked_states = [np.full(base.LIKE_ITEMS, n_items, dtype=np.uint16)]\n disliked_states = [\n np.full(base.DISLIKE_ITEMS, n_items, dtype=np.uint16)\n ]\n state_counts: list[tuple[int, int]] = [(0, 0)]\n user_ids: list[int] = []\n offsets = [0]\n timestamp_parts: list[np.ndarray] = []\n item_parts: list[np.ndarray] = []\n event_parts: list[np.ndarray] = []\n activity_parts: list[np.ndarray] = []\n state_parts: list[np.ndarray] = []\n\n listen_paths = [data / f\"{prefix}_listens.parquet\" for prefix in prefixes]\n started = time.monotonic()\n for seen_users, (uid, listens) in enumerate(\n base.merge_listen_streams(listen_paths, item_index), start=1\n ):\n parts = build_sequence_parts(\n listens,\n explicit_by_user.get(uid, base.empty_explicit_events()),\n n_items,\n liked_states,\n disliked_states,\n state_counts,\n )\n if parts is None:\n continue\n sequence_row = len(user_ids)\n user_ids.append(uid)\n timestamp_parts.append(parts[0].astype(np.uint32, copy=False))\n item_parts.append(parts[1])\n event_parts.append(parts[2])\n activity_parts.append(parts[3])\n state_parts.append(parts[4])\n offsets.append(offsets[-1] + len(parts[1]))\n inference_row = inference_rows.get(uid)\n if inference_row is not None:\n inference_sequence_rows[inference_row] = sequence_row\n inference_user_features[inference_row] = final_user_features(\n parts[0],\n parts[1],\n parts[2],\n history_end,\n int(like_counts[inference_row]),\n int(dislike_counts[inference_row]),\n )\n if seen_users % 1_000 == 0:\n log(\n f\"prepared users={seen_users:,} tokens={offsets[-1]:,} \"\n f\"states={len(liked_states):,} \"\n f\"elapsed={time.monotonic() - started:.1f}s\"\n )\n\n missing = np.flatnonzero(inference_sequence_rows < 0)\n if len(missing):\n inference_user_features[missing, 4] = np.log1p(like_counts[missing]) / 6.0\n inference_user_features[missing, 5] = (\n np.log1p(dislike_counts[missing]) / 4.0\n )\n log(f\"target users without qualifying catalog sequence={len(missing):,}\")\n\n prepared = SequenceData(\n item_ids=item_ids,\n item_features=base.build_item_features(catalog),\n user_ids=np.asarray(user_ids, dtype=np.int64),\n offsets=np.asarray(offsets, dtype=np.int64),\n sequence_timestamps=np.concatenate(timestamp_parts),\n sequence_items=np.concatenate(item_parts),\n event_features=np.concatenate(event_parts),\n activity_features=np.concatenate(activity_parts),\n state_ids=np.concatenate(state_parts),\n liked_states=np.stack(liked_states),\n disliked_states=np.stack(disliked_states),\n state_counts=np.asarray(state_counts, dtype=np.int32),\n inference_user_rows=inference_sequence_rows,\n inference_liked_items=inference_likes,\n inference_disliked_items=inference_dislikes,\n inference_user_features=inference_user_features,\n )\n log(\n f\"prepared stage={stage} sequence_users={len(prepared.user_ids):,} \"\n f\"tokens={len(prepared.sequence_items):,} \"\n f\"next_item_targets={prepared.target_count:,} \"\n f\"states={len(prepared.liked_states):,}\"\n )\n return prepared, eligibility\n\n\ndef cache_metadata(stage: str, config: Config) -> dict[str, object]:\n return {\n \"cache_version\": CACHE_VERSION,\n \"stage\": stage,\n \"positive_listen_threshold\": base.LISTEN_POSITIVE_THRESHOLD,\n \"profile_lookback_days\": base.PROFILE_LOOKBACK_DAYS,\n \"event_time_feature\": \"exp-negative-gap-over-60-days\",\n }\n\n\ndef save_cache(\n path: Path, prepared: SequenceData, stage: str, config: Config\n) -> None:\n path.parent.mkdir(parents=True, exist_ok=True)\n np.savez(\n path,\n metadata=np.asarray(json.dumps(cache_metadata(stage, config), sort_keys=True)),\n **prepared.__dict__,\n )\n log(f\"saved public-derived full-sequence cache: {path}\")\n\n\ndef load_cache(path: Path, stage: str, config: Config) -> SequenceData:\n with np.load(path, allow_pickle=False) as artifact:\n actual = json.loads(str(artifact[\"metadata\"].item()))\n expected = cache_metadata(stage, config)\n if actual != expected:\n raise RuntimeError(f\"sequence cache differs: {actual} != {expected}\")\n values = {\n field: artifact[field].copy()\n for field in SequenceData.__dataclass_fields__\n }\n prepared = SequenceData(**values)\n log(\n f\"loaded full-sequence cache: tokens={len(prepared.sequence_items):,} \"\n f\"targets={prepared.target_count:,}\"\n )\n return prepared\n\n\ndef load_or_prepare(\n public: Path,\n stage: str,\n config: Config,\n cache: Path | None,\n) -> tuple[SequenceData, CandidateSet]:\n candidate_name = (\n \"validation_eligible_candidates.npz\"\n if stage == \"validation\"\n else \"test_eligible_candidates.npz\"\n )\n eligibility = CandidateSet.read(public / \"data\" / candidate_name)\n if cache is not None and cache.is_file():\n prepared = load_cache(cache, stage, config)\n if not np.array_equal(prepared.item_ids, eligibility.item_ids):\n raise RuntimeError(\"cached catalog differs from candidate artifact\")\n return prepared, eligibility\n prepared, built_eligibility = prepare_data(public, stage, config)\n if cache is not None:\n save_cache(cache, prepared, stage, config)\n return prepared, built_eligibility\n\n\ndef build_item_correction(\n prepared: SequenceData,\n config: Config,\n target_starts: np.ndarray,\n target_counts: np.ndarray,\n) -> torch.Tensor:\n \"\"\"Estimate in-batch propensities from the selected training targets.\"\"\"\n counts = np.zeros(len(prepared.item_ids), dtype=np.float64)\n for user_row, count in enumerate(target_counts):\n if not count:\n continue\n offset = int(prepared.offsets[user_row])\n target_start = int(target_starts[user_row])\n counts += np.bincount(\n prepared.sequence_items[\n offset\n + target_start\n + 1 : offset\n + target_start\n + int(count)\n + 1\n ].astype(np.int64),\n minlength=len(prepared.item_ids),\n )\n total = float(counts.sum())\n probabilities = (counts + 1.0) / (total + len(counts))\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 liked_items = torch.from_numpy(\n prepared.liked_states[unique_states].astype(np.int64)\n ).to(device)\n disliked_items = torch.from_numpy(\n prepared.disliked_states[unique_states].astype(np.int64)\n ).to(device)\n liked_vectors = model._mean_context_channel(liked_items)\n disliked_vectors = model._mean_context_channel(disliked_items)\n inverse_rows = torch.from_numpy(inverse.astype(np.int64)).to(device)\n counts = prepared.state_counts[state_ids]\n return (\n liked_vectors[inverse_rows],\n disliked_vectors[inverse_rows],\n torch.from_numpy(counts.astype(np.float32)).to(device),\n )\n\n\ndef user_queries(\n model: base.SequentialTwoTower,\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 history_training = totals - validation\n training = history_training.copy()\n if max_train_targets_per_user:\n training = np.minimum(training, max_train_targets_per_user)\n window_starts = retained_target_window_starts(totals, validation, training)\n return TargetPartition(\n window_starts=window_starts,\n training_counts=training,\n validation_counts=validation,\n history_training_counts=history_training,\n )\n\n\ndef load_organic_like_outcomes(\n public: Path, stage: str, item_ids: np.ndarray\n) -> dict[int, dict[int, np.ndarray]]:\n \"\"\"Load mapped organic-like timestamps from public fitting history.\"\"\"\n prefixes = (\"train\",) if stage == \"validation\" else (\"train\", \"val\")\n frames = [\n pd.read_parquet(\n public / \"data\" / f\"{prefix}_likes.parquet\",\n columns=[\"uid\", \"timestamp\", \"item_id\", \"is_organic\"],\n )\n for prefix in prefixes\n ]\n likes = pd.concat(frames, ignore_index=True)\n likes = likes[likes.is_organic == 1].copy()\n mapped = pd.Index(item_ids).get_indexer(\n likes.item_id.to_numpy(dtype=np.int64)\n )\n likes = likes.loc[mapped >= 0, [\"uid\", \"timestamp\"]].copy()\n likes[\"item\"] = mapped[mapped >= 0].astype(np.int32, copy=False)\n likes = likes.sort_values([\"uid\", \"item\", \"timestamp\"], kind=\"stable\")\n nested: dict[int, dict[int, list[int]]] = {}\n for raw_uid, raw_timestamp, raw_item in likes.itertuples(\n index=False, name=None\n ):\n nested.setdefault(int(raw_uid), {}).setdefault(int(raw_item), []).append(\n int(raw_timestamp)\n )\n result = {\n uid: {\n item: np.asarray(timestamps, dtype=np.int64)\n for item, timestamps in items.items()\n }\n for uid, items in nested.items()\n }\n log(\n f\"loaded organic-like outcomes: rows={len(likes):,} \"\n f\"users={len(result):,}\"\n )\n return result\n\n\ndef build_target_weights(\n public: Path,\n stage: str,\n prepared: SequenceData,\n training_counts: np.ndarray,\n config: Config,\n) -> np.ndarray:\n \"\"\"Build nonzero per-token weights without dropping adjacent targets.\"\"\"\n if config.target_weighting == \"uniform\":\n weights = np.ones(len(prepared.sequence_items), dtype=np.float32)\n else:\n weights = np.full(\n len(prepared.sequence_items),\n config.ordinary_target_weight,\n dtype=np.float32,\n )\n organic_strong = (\n (prepared.event_features[:, 0] > 0.5)\n & (prepared.event_features[:, 1] >= 0.8)\n )\n weights[organic_strong] = config.organic_strong_target_weight\n if config.target_weighting == \"discovery-graded\":\n like_outcomes = load_organic_like_outcomes(\n public, stage, prepared.item_ids\n )\n history_end = (\n base.TRAIN_END if stage == \"validation\" else base.VALIDATION_END\n )\n horizon = config.discovery_horizon_days * base.DAY\n grade_three = 0\n for user_row, uid in enumerate(prepared.user_ids):\n start = int(prepared.offsets[user_row])\n end = int(prepared.offsets[user_row + 1])\n if end - start < 2:\n continue\n last_training_target = start + int(training_counts[user_row])\n first_validation_target = last_training_target + 1\n split_timestamp = (\n int(prepared.sequence_timestamps[first_validation_target])\n if first_validation_target < end\n else history_end\n )\n user_likes = like_outcomes.get(int(uid), {})\n seen = {int(prepared.sequence_items[start])}\n for position in range(start + 1, end):\n item = int(prepared.sequence_items[position])\n novel = item not in seen\n seen.add(item)\n if not novel:\n # Repeats remain training examples, but they cannot be\n # eligible discovery outcomes at the corresponding\n # cutoff and therefore keep only the ordinary weight.\n weights[position] = config.ordinary_target_weight\n continue\n timestamps = user_likes.get(item)\n if timestamps is None:\n continue\n timestamp = int(prepared.sequence_timestamps[position])\n upper = min(\n timestamp + horizon,\n split_timestamp\n if position <= last_training_target\n else history_end,\n )\n outcome = int(np.searchsorted(timestamps, timestamp, side=\"left\"))\n if outcome < len(timestamps) and int(timestamps[outcome]) < upper:\n weights[position] = config.organic_like_target_weight\n grade_three += 1\n log(\n f\"grade-3 first-encounter targets={grade_three:,} \"\n f\"horizon_days={config.discovery_horizon_days}\"\n )\n log(\n f\"target weighting={config.target_weighting} \"\n f\"min={weights.min():.3f} mean={weights.mean():.3f} \"\n f\"max={weights.max():.3f}\"\n )\n return weights\n\n\ndef build_dense_block(\n prepared: SequenceData,\n target_weights: np.ndarray,\n users: np.ndarray,\n window_starts: np.ndarray,\n process_counts: np.ndarray,\n score_starts: np.ndarray,\n start: int,\n steps: int,\n config: Config,\n) -> tuple[np.ndarray, ...]:\n \"\"\"Build one causal sequence block and mark the target rows to score.\"\"\"\n batch_size = len(users)\n padding_item = len(prepared.item_ids)\n input_items = np.full((batch_size, steps), padding_item, dtype=np.uint16)\n event_features = np.zeros((batch_size, steps, 3), dtype=np.float16)\n activity_features = np.zeros((batch_size, steps, 4), dtype=np.float16)\n state_ids = np.zeros((batch_size, steps), dtype=np.uint32)\n targets = np.zeros((batch_size, steps), dtype=np.uint16)\n weights = np.ones((batch_size, steps), dtype=np.float32)\n scored = np.zeros((batch_size, steps), dtype=bool)\n histories = np.full(\n (batch_size, steps, config.bptt_steps),\n padding_item,\n dtype=np.uint16,\n )\n processed = np.zeros(batch_size, dtype=np.int32)\n\n lengths = prepared.lengths\n for row, user_row in enumerate(users):\n count = min(steps, max(int(process_counts[row]) - start, 0))\n if not count:\n continue\n offset = int(prepared.offsets[user_row])\n window_start = int(window_starts[row])\n absolute_start = window_start + start\n positions = slice(\n offset + absolute_start,\n offset + absolute_start + count,\n )\n target_positions = slice(\n offset + absolute_start + 1,\n offset + absolute_start + count + 1,\n )\n input_items[row, :count] = prepared.sequence_items[positions]\n event_features[row, :count] = prepared.event_features[positions]\n activity_features[row, :count] = prepared.activity_features[positions]\n state_ids[row, :count] = prepared.state_ids[positions]\n targets[row, :count] = prepared.sequence_items[target_positions]\n weights[row, :count] = target_weights[target_positions]\n score_from = min(count, max(int(score_starts[row]) - start, 0))\n scored[row, score_from:count] = True\n processed[row] = count\n user_sequence = prepared.sequence_items[\n offset : offset + int(lengths[user_row])\n ]\n histories[row, :count] = history_windows(\n user_sequence,\n absolute_start,\n count,\n config.bptt_steps,\n padding_item,\n )\n return (\n input_items,\n event_features,\n activity_features,\n state_ids,\n targets,\n scored,\n histories,\n processed,\n weights,\n )\n\n\ndef score_target_microbatches(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n config: Config,\n sequence_vectors: torch.Tensor,\n flat_states: np.ndarray,\n flat_activity: np.ndarray,\n positive_items: torch.Tensor,\n negatives: torch.Tensor,\n negative_weights: torch.Tensor,\n sample_weights: np.ndarray,\n device: torch.device,\n backward: bool,\n) -> tuple[dict[str, float], torch.Tensor | None]:\n \"\"\"Compute one logical block's exact BCE in bounded-memory slices.\"\"\"\n target_count = len(positive_items)\n total_weight = float(sample_weights.sum())\n if total_weight <= 0:\n raise RuntimeError(\"target block has no positive sample weight\")\n sequence_gradient = torch.zeros_like(sequence_vectors) if backward else None\n statistics = {\n \"loss_sum\": 0.0,\n \"positive_sum\": 0.0,\n \"negative_sum\": 0.0,\n \"pair_correct\": 0.0,\n \"weight_sum\": 0.0,\n }\n for begin in range(0, target_count, config.loss_microbatch_size):\n end = min(begin + config.loss_microbatch_size, target_count)\n selected_states = flat_states[begin:end]\n if backward:\n sequence_input = (\n sequence_vectors[begin:end].detach().requires_grad_(True)\n )\n else:\n sequence_input = sequence_vectors[begin:end]\n liked_vectors, disliked_vectors, _ = pool_explicit_states(\n model, prepared, selected_states, device\n )\n counts = prepared.state_counts[selected_states]\n numeric = np.zeros((end - begin, 6), dtype=np.float32)\n numeric[:, :4] = flat_activity[begin:end]\n numeric[:, 4] = np.log1p(counts[:, 0]) / 6.0\n numeric[:, 5] = np.log1p(counts[:, 1]) / 4.0\n query_vectors = user_queries(\n model,\n sequence_input,\n liked_vectors,\n disliked_vectors,\n torch.from_numpy(numeric).to(device),\n )\n positive_vectors, negative_vectors = candidate_vectors(\n model,\n positive_items[begin:end],\n negatives[begin:end],\n )\n positive_logits = (query_vectors * positive_vectors).sum(dim=1)\n negative_logits = torch.einsum(\n \"bd,bkd->bk\", query_vectors, negative_vectors\n )\n positive_loss = F.softplus(-positive_logits)\n negative_loss = corrected_negative_loss(\n negative_logits, negative_weights[begin:end], config\n )\n selected_weights = torch.from_numpy(\n sample_weights[begin:end].astype(np.float32, copy=False)\n ).to(device)\n loss_sum = (\n selected_weights * (positive_loss + negative_loss) / 2.0\n ).sum()\n if not bool(torch.isfinite(loss_sum)):\n raise RuntimeError(\"dense next-item training produced non-finite loss\")\n if backward:\n (loss_sum / total_weight).backward()\n if sequence_input.grad is None:\n raise RuntimeError(\"sequence microbatch did not produce a gradient\")\n sequence_gradient[begin:end] = sequence_input.grad\n statistics[\"loss_sum\"] += float(loss_sum.detach().cpu())\n statistics[\"positive_sum\"] += float(positive_logits.detach().sum().cpu())\n statistics[\"negative_sum\"] += float(negative_logits.detach().sum().cpu())\n statistics[\"pair_correct\"] += float(\n (positive_logits.detach()[:, None] > negative_logits.detach()).sum().cpu()\n )\n statistics[\"weight_sum\"] += float(selected_weights.sum().detach().cpu())\n return statistics, sequence_gradient\n\n\ndef release_accelerator_cache(device: torch.device) -> None:\n if device.type == \"mps\":\n torch.mps.empty_cache()\n\n\ndef run_training_epoch(\n model: base.SequentialTwoTower,\n optimizer: torch.optim.Optimizer,\n prepared: SequenceData,\n config: Config,\n target_weights: np.ndarray,\n window_starts: np.ndarray,\n target_counts: np.ndarray,\n item_correction: torch.Tensor,\n known_positive_packed: np.ndarray | None,\n generator: torch.Generator,\n device: torch.device,\n epoch: int,\n) -> dict[str, float]:\n model.train()\n train_users = np.flatnonzero(target_counts > 0)\n sorted_users = train_users[\n np.argsort(target_counts[train_users], kind=\"stable\")\n ]\n groups = [\n sorted_users[start : start + config.user_batch_size]\n for start in range(0, len(sorted_users), config.user_batch_size)\n ]\n group_order = torch.randperm(len(groups), generator=generator).numpy()\n negative_count = config.uniform_negatives + config.inbatch_negatives\n total_loss = 0.0\n total_targets = 0\n total_weight = 0.0\n total_positive = 0.0\n total_negative = 0.0\n total_pair_correct = 0.0\n batches = 0\n started = time.monotonic()\n\n for group_number in group_order:\n users = groups[int(group_number)]\n group_counts = target_counts[users]\n hidden = torch.zeros(\n 1, len(users), config.embedding_dim, device=device\n )\n max_targets = int(group_counts.max())\n for start in range(0, max_targets, config.bptt_steps):\n steps = min(config.bptt_steps, max_targets - start)\n block = build_dense_block(\n prepared,\n target_weights,\n users,\n window_starts[users],\n group_counts,\n np.zeros(len(users), dtype=np.int64),\n start,\n steps,\n config,\n )\n sequence_outputs, _ = event_sequence(\n model,\n torch.from_numpy(block[0].astype(np.int64)).to(device),\n torch.from_numpy(block[1].astype(np.float32)).to(device),\n hidden,\n )\n hidden = torch.stack(\n [\n sequence_outputs[row, count - 1]\n if count\n else hidden[0, row]\n for row, count in enumerate(block[7])\n ]\n ).unsqueeze(0).detach()\n flat_scored = block[5].reshape(-1)\n if not np.any(flat_scored):\n continue\n mask = torch.from_numpy(flat_scored).to(device)\n sequence_vectors = sequence_outputs.reshape(\n -1, config.embedding_dim\n )[mask]\n flat_states = block[3].reshape(-1)[flat_scored]\n positive_items = torch.from_numpy(\n block[4].reshape(-1)[flat_scored].astype(np.int64)\n ).to(device)\n known_history = torch.from_numpy(\n block[6].reshape(-1, config.bptt_steps)[flat_scored].astype(\n np.int64\n )\n ).to(device)\n liked_items = torch.from_numpy(\n prepared.liked_states[flat_states].astype(np.int64)\n ).to(device)\n flat_user_rows = np.broadcast_to(\n users[:, None], block[5].shape\n ).reshape(-1)[flat_scored]\n negatives, negative_weights = draw_training_negatives(\n positive_items,\n known_history,\n liked_items,\n flat_user_rows,\n known_positive_packed,\n prepared,\n config,\n item_correction,\n generator,\n device,\n )\n optimizer.zero_grad(set_to_none=True)\n statistics, sequence_gradient = score_target_microbatches(\n model,\n prepared,\n config,\n sequence_vectors,\n flat_states,\n block[2].reshape(-1, 4)[flat_scored].astype(np.float32),\n positive_items,\n negatives,\n negative_weights,\n block[8].reshape(-1)[flat_scored],\n device,\n backward=True,\n )\n if sequence_gradient is None:\n raise RuntimeError(\"training block omitted its sequence gradient\")\n sequence_vectors.backward(sequence_gradient)\n nn.utils.clip_grad_norm_(model.parameters(), config.gradient_clip)\n optimizer.step()\n\n count = len(positive_items)\n total_loss += statistics[\"loss_sum\"]\n total_targets += count\n total_weight += statistics[\"weight_sum\"]\n total_positive += statistics[\"positive_sum\"]\n total_negative += statistics[\"negative_sum\"]\n total_pair_correct += statistics[\"pair_correct\"]\n batches += 1\n if batches % 25 == 0:\n release_accelerator_cache(device)\n if batches % 250 == 0:\n batch_bce = statistics[\"loss_sum\"] / max(\n statistics[\"weight_sum\"], 1.0e-12\n )\n running_bce = total_loss / max(total_weight, 1.0e-12)\n log(\n f\"epoch={epoch}/{config.epochs} batches={batches:,} \"\n f\"targets={total_targets:,} \"\n f\"batch_bce={batch_bce:.6f} \"\n f\"running_bce={running_bce:.6f} \"\n f\"elapsed={time.monotonic() - started:.1f}s\"\n )\n\n return {\n \"epoch\": float(epoch),\n \"loss\": total_loss / max(total_weight, 1.0e-12),\n \"positive_logit\": total_positive / max(total_targets, 1),\n \"negative_logit\": total_negative / max(total_targets * negative_count, 1),\n \"pair_accuracy\": total_pair_correct\n / max(total_targets * negative_count, 1),\n \"targets\": float(total_targets),\n \"target_weight_sum\": total_weight,\n \"batches\": float(batches),\n \"seconds\": time.monotonic() - started,\n }\n\n\[email protected]_grad()\ndef evaluate_validation_bce(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n config: Config,\n target_weights: np.ndarray,\n window_starts: np.ndarray,\n training_counts: np.ndarray,\n validation_counts: np.ndarray,\n item_correction: torch.Tensor,\n known_positive_packed: np.ndarray | None,\n device: torch.device,\n) -> dict[str, float]:\n \"\"\"Measure deterministic BCE on each user's chronological target suffix.\"\"\"\n model.eval()\n totals = training_counts + validation_counts\n users_with_validation = np.flatnonzero(validation_counts > 0)\n ordered = users_with_validation[\n np.argsort(totals[users_with_validation], kind=\"stable\")\n ]\n groups = [\n ordered[start : start + config.user_batch_size]\n for start in range(0, len(ordered), config.user_batch_size)\n ]\n generator = torch.Generator(device=\"cpu\").manual_seed(config.seed + 20_000)\n total_loss = 0.0\n total_targets = 0\n total_weight = 0.0\n started = time.monotonic()\n\n for group_number, users in enumerate(groups, start=1):\n group_totals = totals[users]\n group_starts = training_counts[users]\n hidden = torch.zeros(\n 1, len(users), config.embedding_dim, device=device\n )\n max_targets = int(group_totals.max())\n for start in range(0, max_targets, config.bptt_steps):\n steps = min(config.bptt_steps, max_targets - start)\n block = build_dense_block(\n prepared,\n target_weights,\n users,\n window_starts[users],\n group_totals,\n group_starts,\n start,\n steps,\n config,\n )\n sequence_outputs, _ = event_sequence(\n model,\n torch.from_numpy(block[0].astype(np.int64)).to(device),\n torch.from_numpy(block[1].astype(np.float32)).to(device),\n hidden,\n )\n hidden = torch.stack(\n [\n sequence_outputs[row, count - 1]\n if count\n else hidden[0, row]\n for row, count in enumerate(block[7])\n ]\n ).unsqueeze(0)\n flat_scored = block[5].reshape(-1)\n if not np.any(flat_scored):\n continue\n mask = torch.from_numpy(flat_scored).to(device)\n sequence_vectors = sequence_outputs.reshape(\n -1, config.embedding_dim\n )[mask]\n flat_states = block[3].reshape(-1)[flat_scored]\n positive_items = torch.from_numpy(\n block[4].reshape(-1)[flat_scored].astype(np.int64)\n ).to(device)\n known_history = torch.from_numpy(\n block[6].reshape(-1, config.bptt_steps)[flat_scored].astype(\n np.int64\n )\n ).to(device)\n liked_items = torch.from_numpy(\n prepared.liked_states[flat_states].astype(np.int64)\n ).to(device)\n flat_user_rows = np.broadcast_to(\n users[:, None], block[5].shape\n ).reshape(-1)[flat_scored]\n negatives, negative_weights = draw_training_negatives(\n positive_items,\n known_history,\n liked_items,\n flat_user_rows,\n known_positive_packed,\n prepared,\n config,\n item_correction,\n generator,\n device,\n )\n statistics, _ = score_target_microbatches(\n model,\n prepared,\n config,\n sequence_vectors,\n flat_states,\n block[2].reshape(-1, 4)[flat_scored].astype(np.float32),\n positive_items,\n negatives,\n negative_weights,\n block[8].reshape(-1)[flat_scored],\n device,\n backward=False,\n )\n total_loss += statistics[\"loss_sum\"]\n total_targets += len(positive_items)\n total_weight += statistics[\"weight_sum\"]\n if group_number % 25 == 0:\n release_accelerator_cache(device)\n return {\n \"validation_loss\": total_loss / max(total_weight, 1.0e-12),\n \"validation_targets\": float(total_targets),\n \"validation_target_weight_sum\": total_weight,\n \"validation_seconds\": time.monotonic() - started,\n }\n\n\ndef snapshot_model(model: nn.Module) -> dict[str, torch.Tensor]:\n return {\n name: value.detach().cpu().clone()\n for name, value in model.state_dict().items()\n }\n\n\ndef save_epoch_checkpoint(\n checkpoint_dir: Path,\n epoch: int,\n model_state: dict[str, torch.Tensor],\n config: Config,\n diagnostics: dict[str, float],\n optimizer: torch.optim.Optimizer,\n generator: torch.Generator,\n) -> Path:\n checkpoint_dir.mkdir(parents=True, exist_ok=True)\n path = checkpoint_dir / f\"epoch-{epoch:02d}.pt\"\n torch.save(\n {\n \"epoch\": epoch,\n \"config\": asdict(config),\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\n )\n ordered = np.argsort(lengths, kind=\"stable\")\n result = torch.zeros(\n len(sequence_rows), config.embedding_dim, dtype=torch.float32\n )\n for group_start in range(0, len(ordered), config.user_batch_size):\n output_rows = ordered[group_start : group_start + config.user_batch_size]\n rows = sequence_rows[output_rows]\n group_lengths = lengths[output_rows]\n hidden = torch.zeros(\n 1, len(output_rows), config.embedding_dim, device=device\n )\n for start in range(0, int(group_lengths.max(initial=0)), config.bptt_steps):\n steps = min(\n config.bptt_steps,\n int(group_lengths.max(initial=0)) - start,\n )\n items = np.full(\n (len(output_rows), steps),\n len(prepared.item_ids),\n dtype=np.uint16,\n )\n features = np.zeros((len(output_rows), steps, 3), dtype=np.float16)\n counts = np.zeros(len(output_rows), dtype=np.int32)\n for local_row, sequence_row in enumerate(rows):\n if sequence_row < 0:\n continue\n count = min(steps, max(int(group_lengths[local_row]) - start, 0))\n if not count:\n continue\n offset = int(prepared.offsets[sequence_row])\n selected = slice(offset + start, offset + start + count)\n items[local_row, :count] = prepared.sequence_items[selected]\n features[local_row, :count] = prepared.event_features[selected]\n counts[local_row] = count\n outputs, _ = event_sequence(\n model,\n torch.from_numpy(items.astype(np.int64)).to(device),\n torch.from_numpy(features.astype(np.float32)).to(device),\n hidden,\n )\n next_rows: list[torch.Tensor] = []\n for local_row, count in enumerate(counts):\n next_rows.append(\n outputs[local_row, count - 1]\n if count\n else hidden[0, local_row]\n )\n hidden = torch.stack(next_rows).unsqueeze(0)\n\n liked_items = torch.from_numpy(\n prepared.inference_liked_items[output_rows].astype(np.int64)\n ).to(device)\n disliked_items = torch.from_numpy(\n prepared.inference_disliked_items[output_rows].astype(np.int64)\n ).to(device)\n queries = user_queries(\n model,\n hidden.squeeze(0),\n model._mean_context_channel(liked_items),\n model._mean_context_channel(disliked_items),\n torch.from_numpy(\n prepared.inference_user_features[output_rows].astype(np.float32)\n ).to(device),\n )\n result[torch.from_numpy(output_rows.astype(np.int64))] = queries.cpu()\n return result\n\n\[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\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n",
"exit_code": 0,
"duration": {
"secs": 0,
"nanos": 3460
},
"formatted_output": "Warning: truncated output (original token count: 20419)\nTotal output lines: 2197\n\n/app\n/app/solution:\ntotal 8\ndrwxr-xr-x 2 agent agent 4096 Sep 23 05:16 .\ndrwxr-xr-x 3 agent agent 4096 Sep 23 05:16 ..\n\n/task/current_model:\ntotal 104\ndr-xr-xr-x 1 root root 4096 Sep 23 05:14 .\ndr-xr-xr-x 1 root root 4096 Sep 23 05:16 ..\n-r--r--r-- 1 root root 80058 Sep 23 05:14 train_current_model.py\n-r--r--r-- 1 root root 15985 Sep 23 05:14 two_tower_model.py\n\n/task/data:\ntotal 347392\ndr-xr-xr-x 1 root root 4096 Sep 23 05:15 .\ndr-xr-xr-x 1 root root 4096 Sep 23 05:16 ..\n-r--r--r-- 1 root root 349352 Sep 23 05:14 candidate_catalog.parquet\n-r--r--r-- 1 root root 84127 Sep 23 05:14 target_users.parquet\n-r--r--r-- 1 root root 7736341 Sep 23 05:14 test_eligible_candidates.npz\n-r--r--r-- 1 root root 1092329 Sep 23 05:15 test_preference_state.parquet\n-r--r--r-- 1 root root 857213 Sep 23 05:14 train_dislikes.parquet\n-r--r--r-- 1 root root 5374652 Sep 23 05:14 train_likes.parquet\n-r--r--r-- 1 root root 319183255 Sep 23 05:14 train_listens.parquet\n-r--r--r-- 1 root root 155988 Sep 23 05:14 train_undislikes.parquet\n-r--r--r-- 1 root root 1855885 Sep 23 05:14 train_unlikes.parquet\n-r--r--r-- 1 root root 36571 Sep 23 05:14 val_dislikes.parquet\n-r--r--r-- 1 root root 298484 Sep 23 05:14 val_likes.parquet\n-r--r--r-- 1 root root 9947609 Sep 23 05:14 val_listens.parquet\n-r--r--r-- 1 root root 7845 Sep 23 05:14 val_undislikes.parquet\n-r--r--r-- 1 root root 72536 Sep 23 05:14 val_unlikes.parquet\n-r--r--r-- 1 root root 7573821 Sep 23 05:14 validation_eligible_candidates.npz\n-r--r--r-- 1 root root 1061761 Sep 23 05:15 validation_preference_state.parquet\n#!/usr/bin/env python3\n\"\"\"Train the Yambda windowed two-tower baseline.\n\nEvery catalog listen with at least 50 percent completion is a sequence token.\nWithin each selected chronological window, every adjacent target contributes a\nloss. The fast default retains each user's most recent 128 training targets and\n16 held-out validation targets; passing zero for either cap restores the\nuncapped behavior for that side of the split. A 100-step truncated-BPTT block\ncomputes all of its timestep losses and carries the detached GRU hidden state\ninto the next block.\n\nThe model is a pure two-tower retriever: no user-ID embedding, no output\nnormalization, raw dot-product retrieval, sample-weighted binary cross entropy,\nand 24 uniform plus eight propensity-corrected unique in-batch negatives. Every\nselected adjacent target retains a nonzero weight. Only files under the supplied\npublic directory are read.\n\"\"\"\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport random\nimport sys\nimport time\nfrom dataclasses import asdict, dataclass\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch import nn\nfrom torch.nn import functional as F\n\nimport two_tower_model as base\n\n\nCACHE_VERSION = 2\n\n\n@dataclass(frozen=True)\nclass Config:\n bptt_steps: int = 100\n user_batch_size: int = 64\n embedding_dim: int = 192\n hidden_dim: int = 192\n user_hidden_1: int = 512\n user_hidden_2: int = 256\n item_hidden_1: int = 512\n item_hidden_2: int = 256\n dropout: float = 0.05\n epochs: int = 1\n learning_rate: float = 1.0e-3\n weight_decay: float = 1.0e-5\n gradient_clip: float = 2.0\n loss_microbatch_size: int = 65_536\n uniform_negatives: int = 24\n inbatch_negatives: int = 8\n inbatch_correction_power: float = 0.5\n inbatch_correction_min: float = 0.25\n inbatch_correction_max: float = 4.0\n negative_filter_scope: str = \"full-training-history\"\n target_weighting: str = \"organic-strong\"\n ordinary_target_weight: float = 0.25\n organic_strong_target_weight: float = 1.0\n organic_like_target_weight: float = 3.0\n discovery_horizon_days: int = 7\n validation_fraction: float = 0.10\n max_train_targets_per_user: int = 128\n max_validation_targets_per_user: int = 16\n early_stopping_patience: int = 1\n early_stopping_min_delta: float = 1.0e-4\n seed: int = 29\n\n\n@dataclass(frozen=True)\nclass CandidateSet:\n uids: np.ndarray\n item_ids: np.ndarray\n packed: np.ndarray\n\n @classmethod\n def read(cls, path: Path) -> \"CandidateSet\":\n with np.load(path, allow_pickle=False) as artifact:\n result = cls(\n uids=artifact[\"uids\"].astype(np.int64, copy=True),\n item_ids=artifact[\"item_ids\"].astype(np.int64, copy=True),\n packed=artifact[\"packed_eligible\"].astype(np.uint8, copy=True),\n )\n expected = (len(result.uids), (len(result.item_ids) + 7) // 8)\n if result.packed.shape != expected:\n raise ValueError(\n f\"invalid candidate artifact shape {result.packed.shape}; \"\n f\"expected {expected}\"\n )\n return result\n\n\n@dataclass(frozen=True)\nclass SequenceData:\n item_ids: np.ndarray\n item_features: np.ndarray\n user_ids: np.ndarray\n offsets: np.ndarray\n sequence_timestamps: np.ndarray\n sequence_items: np.ndarray\n event_features: np.ndarray\n activity_features: np.ndarray\n state_ids: np.ndarray\n liked_states: np.ndarray\n disliked_states: np.ndarray\n state_counts: np.ndarray\n inference_user_rows: np.ndarray\n inference_liked_items: np.ndarray\n inference_disliked_items: np.ndarray\n inference_user_features: np.ndarray\n\n @property\n def lengths(self) -> np.ndarray:\n return np.diff(self.offsets)\n\n @property\n def target_count(self) -> int:\n return int(np.maximum(self.lengths - 1, 0).sum())\n\n\n@dataclass(frozen=True)\nclass TargetPartition:\n \"\"\"A contiguous recent target window and its chronological BCE split.\"\"\"\n\n window_starts: np.ndarray\n training_counts: np.ndarray\n validation_counts: np.ndarray\n history_training_counts: np.ndarray\n\n @property\n def selected_count(self) -> int:\n return int(self.training_counts.sum() + self.validation_counts.sum())\n\n\ndef log(message: str) -> None:\n print(message, file=sys.stderr, flush=True)\n\n\ndef seed_everything(seed: int) -> None:\n random.seed(seed)\n np.random.seed(seed)\n torch.manual_seed(seed)\n\n\ndef model_config(config: Config) -> base.TowerConfig:\n \"\"\"Map the dense trainer configuration onto the unchanged tower.\"\"\"\n return base.TowerConfig(\n sequence_length=config.bptt_steps,\n embedding_dim=config.embedding_dim,\n hidden_dim=config.hidden_dim,\n user_hidden_1=config.user_hidden_1,\n user_hidden_2=config.user_hidden_2,\n item_hidden_1=config.item_hidden_1,\n item_hidden_2=config.item_hidden_2,\n dropout=config.dropout,\n epochs=config.epochs,\n learning_rate=config.learning_rate,\n weight_decay=config.weight_decay,\n gradient_clip=config.gradient_clip,\n uniform_negatives=config.uniform_negatives,\n inbatch_negatives=config.inbatch_negatives,\n inbatch_correction_power=config.inbatch_correction_power,\n inbatch_correction_min=config.inbatch_correction_min,\n inbatch_correction_max=config.inbatch_correction_max,\n seed=config.seed,\n )\n\n\ndef append_state_snapshot(\n active_likes: dict[int, int],\n active_dislikes: dict[int, int],\n n_items: int,\n liked_states: list[np.ndarray],\n disliked_states: list[np.ndarray],\n state_counts: list[tuple[int, int]],\n) -> int:\n liked_states.append(base.pack_active_items(active_likes, base.LIKE_ITEMS, n_items))\n disliked_states.append(\n base.pack_active_items(active_dislikes, base.DISLIKE_ITEMS, n_items)\n )\n state_counts.append((len(active_likes), len(active_dislikes)))\n return len(liked_states) - 1\n\n\ndef causal_activity_features(\n timestamps: np.ndarray,\n items: np.ndarray,\n organic: np.ndarray,\n completion: np.ndarray,\n) -> np.ndarray:\n \"\"\"Compute the first four reviewed user features at every event cutoff.\"\"\"\n length = len(items)\n result = np.zeros((length, 4), dtype=np.float16)\n if not length:\n return result\n left_rows = np.searchsorted(\n timestamps,\n timestamps - base.PROFILE_LOOKBACK_DAYS * base.DAY,\n side=\"left\",\n )\n rows = np.arange(length, dtype=np.int64)\n counts = rows - left_rows + 1\n organic_sum = np.cumsum(organic.astype(np.float64))\n completion_sum = np.cumsum(np.minimum(completion, 100).astype(np.float64))\n before_organic = np.where(\n left_rows > 0, organic_sum[np.maximum(left_rows - 1, 0)], 0.0\n )\n before_completion = np.where(\n left_rows > 0, completion_sum[np.maximum(left_rows - 1, 0)], 0.0\n )\n\n unique_counts = np.empty(length, dtype=np.int32)\n frequencies: dict[int, int] = {}\n left = 0\n for right, raw_item in enumerate(items):\n item = int(raw_item)\n frequencies[item] = frequencies.get(item, 0) + 1\n required_left = int(left_rows[right])\n while left < required_left:\n old_item = int(items[left])\n remaining = frequencies[old_item] - 1\n if remaining:\n frequencies[old_item] = remaining\n else:\n del frequencies[old_item]\n left += 1\n unique_counts[right] = len(frequencies)\n\n result[:, 0] = (np.log1p(counts) / 8.0).astype(np.float16)\n result[:, 1] = (np.log1p(unique_counts) / 6.0).astype(np.float16)\n result[:, 2] = (\n (organic_sum - before_organic) / np.maximum(counts, 1)\n ).astype(np.float16)\n result[:, 3] = (\n (completion_sum - before_completion)\n / np.maximum(counts, 1)\n / 100.0\n ).astype(np.float16)\n return result\n\n\ndef build_sequence_parts(\n listens: base.ListenEvents,\n explicit: base.ExplicitEvents,\n n_items: int,\n liked_states: list[np.ndarray],\n disliked_states: list[np.ndarray],\n state_counts: list[tuple[int, int]],\n) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray] | None:\n qualifying = listens.completion_pct >= base.LISTEN_POSITIVE_THRESHOLD\n timestamps = listens.timestamps[qualifying].astype(np.int64, copy=False)\n items = listens.items[qualifying].astype(np.uint16, copy=False)\n organic = listens.organic[qualifying].astype(np.float32, copy=False)\n completion = listens.completion_pct[qualifying].astype(np.float32, copy=False)\n if not len(items):\n return None\n\n event_features = np.zeros((len(items), 3), dtype=np.float16)\n event_features[:, 0] = organic.astype(np.float16)\n event_features[:, 1] = (\n np.minimum(completion, 100.0) / 100.0\n ).astype(np.float16)\n if len(items) > 1:\n gaps = np.maximum(0, timestamps[1:] - timestamps[:-1])\n event_features[1:, 2] = np.exp(\n -gaps.astype(np.float64) / (60.0 * base.DAY)\n ).astype(np.float16)\n\n activity = causal_activity_features(\n timestamps, items, organic, completion\n )\n states = np.zeros(len(items), dtype=np.uint32)\n active_likes: dict[int, int] = {}\n active_dislikes: dict[int, int] = {}\n explicit_position = 0\n current_state = 0\n for row, timestamp in enumerate(timestamps):\n changed = False\n while (\n explicit_position < len(explicit.timestamps)\n and int(explicit.timestamps[explicit_position]) < int(timestamp)\n ):\n item = int(explicit.items[explicit_position])\n event_timestamp = int(explicit.timestamps[explicit_position])\n destination = (\n active_likes\n if int(explicit.channel[explicit_position]) == 0\n else active_dislikes\n )\n if int(explicit.active[explicit_position]) == 1:\n destination[item] = event_timestamp\n else:\n destination.pop(item, None)\n explicit_position += 1\n changed = True\n if changed:\n current_state = append_state_snapshot(\n active_likes,\n active_dislikes,\n n_items,\n liked_states,\n disliked_states,\n state_counts,\n )\n states[row] = np.uint32(current_state)\n return timestamps, items.copy(), event_features, activity, states\n\n\ndef final_user_features(\n timestamps: np.ndarray,\n items: np.ndarray,\n event_features: np.ndarray,\n history_end: int,\n like_count: int,\n dislike_count: int,\n) -> np.ndarray:\n start = int(\n np.searchsorted(\n timestamps,\n history_end - base.PROFILE_LOOKBACK_DAYS * base.DAY,\n side=\"left\",\n )\n )\n result = np.zeros(6, dtype=np.float32)\n recent_items = items[start:]\n if len(recent_items):\n result[0] = np.log1p(len(recent_items)) / 8.0\n result[1] = np.log1p(len(np.unique(recent_items))) / 6.0\n result[2] = float(np.mean(event_features[start:, 0]))\n result[3] = float(np.mean(event_features[start:, 1]))\n result[4] = np.log1p(like_count) / 6.0\n result[5] = np.log1p(dislike_count) / 4.0\n return result\n\n\ndef prepare_data(\n public: Path, stage: str, config: Config\n) -> tuple[SequenceData, CandidateSet]:\n data = public / \"data\"\n catalog = pd.read_parquet(data / \"candidate_catalog.parquet\").sort_values(\n \"popularity_rank\"\n )\n item_ids = catalog.item_id.to_numpy(dtype=np.int64, copy=True)\n item_index = pd.Index(item_ids)\n n_items = len(item_ids)\n if n_items >= np.iinfo(np.uint16).max:\n raise RuntimeError(\"uint16 item storage is too small for this catalog\")\n\n if stage == \"validation\":\n prefixes = (\"train\",)\n history_end = base.TRAIN_END\n candidate_name = \"validation_eligible_candidates.npz\"\n state_name = \"validation_preference_state.parquet\"\n else:\n prefixes = (\"train\", \"val\")\n history_end = base.VALIDATION_END\n candidate_name = \"test_eligible_candidates.npz\"\n state_name = \"test_preference_state.parquet\"\n eligibility = CandidateSet.read(data / candidate_name)\n if not np.array_equal(eligibility.item_ids, item_ids):\n raise RuntimeError(\"catalog and candidate artifact item order differ\")\n\n explicit_by_user = base.load_explicit_events(data, prefixes, item_index)\n inference_likes, inference_dislikes, like_counts, dislike_counts = (\n base.load_inference_state(\n data / state_name,\n item_index,\n eligibility.uids,\n n_items,\n )\n )\n inference_rows = {int(uid): row for row, uid in enumerate(eligibility.uids)}\n inference_user_features = np.zeros((len(eligibility.uids), 6), dtype=np.float32)\n inference_sequence_rows = np.full(len(eligibility.uids), -1, dtype=np.int32)\n\n # Snapshot zero is the shared empty explicit state.\n liked_states = [np.full(base.LIKE_ITEMS, n_items, dtype=np.uint16)]\n disliked_states = [\n np.full(base.DISLIKE_ITEMS, n_items, dtype=np.uint16)\n ]\n state_counts: list[tuple[int, int]] = [(0, 0)]\n user_ids: list[int] = []\n offsets = [0]\n timestamp_parts: list[np.ndarray] = []\n item_parts: list[np.ndarray] = []\n event_parts: list[np.ndarray] = []\n activity_parts: list[np.ndarray] = []\n state_parts: list[np.ndarray] = []\n\n listen_paths = [data / f\"{prefix}_listens.parquet\" for prefix in prefixes]\n started = time.monotonic()\n for seen_users, (uid, listens) in enumerate(\n base.merge_listen_streams(listen_paths, item_index), start=1\n ):\n parts = build_sequence_parts(\n listens,\n explicit_by_user.get(uid, base.empty_explicit_events()),\n n_items,\n liked_states,\n disliked_states,\n state_counts,\n )\n if parts is None:\n continue\n sequence_row = len(user_ids)\n user_ids.append(uid)\n timestamp_parts.append(parts[0].astype(np.uint32, copy=False))\n item_parts.append(parts[1])\n event_parts.append(parts[2])\n activity_parts.append(parts[3])\n state_parts.append(parts[4])\n offsets.append(offsets[-1] + len(parts[1]))\n inference_row = inference_rows.get(uid)\n if inference_row is not None:\n inference_sequence_rows[inference_row] = sequence_row\n inference_user_features[inference_row] = final_user_features(\n parts[0],\n parts[1],\n parts[2],\n history_end,\n int(like_counts[inference_row]),\n int(dislike_counts[inference_row]),\n )\n if seen_users % 1_000 == 0:\n log(\n f\"prepared users={seen_users:,} tokens={offsets[-1]:,} \"\n f\"states={len(liked_states):,} \"\n f\"elapsed={time.monotonic() - started:.1f}s\"\n )\n\n missing = np.flatnonzero(inference_sequence_rows < 0)\n if len(missing):\n inference_user_features[missing, 4] = np.log1p(like_counts[missing]) / 6.0\n inference_user_features[missing, 5] = (\n np.log1p(dislike_counts[missing]) / 4.0\n )\n log(f\"target users without qualifying catalog sequence={len(missing):,}\")\n\n prepared = SequenceData(\n item_ids=item_ids,\n item_features=base.build_item_features(catalog),\n user_ids=np.asarray(user_ids, dtype=np.int64),\n offsets=np.asarray(offsets, dtype=np.int64),\n sequence_timestamps=np.concatenate(timestamp_parts),\n sequence_items=np.concatenate(item_parts),\n event_features=np.concatenate(event_parts),\n activity_features=np.concatenate(activity_parts),\n state_ids=np.concatenate(state_parts),\n liked_states=np.stack(liked_states),\n disliked_states=np.stack(disliked_states),\n state_counts=np.asarray(state_counts, dtype=np.int32),\n inference_user_rows=inference_sequence_rows,\n inference_liked_items=inference_likes,\n inference_disliked_items=inference_dislikes,\n inference_user_features=inference_user_features,\n )\n log(\n f\"prepared stage={stage} sequence_users={len(prepared.user_ids):,} \"\n f\"tokens={len(prepared.sequence_items):,} \"\n f\"next_item_targets={prepared.target_count:,} \"\n f\"states={len(prepared.liked_states):,}\"\n )\n return prepared, eligibility\n\n\ndef cache_metadata(stage: str, config: Config) -> dict[str, object]:\n return {\n \"cache_version\": CACHE_VERSION,\n \"stage\": stage,\n \"positive_listen_threshold\": base.LISTEN_POSITIVE_THRESHOLD,\n \"profile_lookback_days\": base.PROFILE_LOOKBACK_DAYS,\n \"event_time_feature\": \"exp-negative-gap-over-60-days\",\n }\n\n\ndef save_cache(\n path: Path, prepared: SequenceData, stage: str, config: Config\n) -> None:\n path.parent.mkdir(parents=True, exist_ok=True)\n np.savez(\n path,\n metadata=np.asarray(json.dumps(cache_metadata(stage, config), sort_keys=True)),\n **prepared.__dict__,\n )\n log(f\"saved public-derived full-sequence cache: {path}\")\n\n\ndef load_cache(path: Path, stage: str, config: Config) -> SequenceData:\n with np.load(path, allow_pickle=False) as artifact:\n actual = json.loads(str(artifact[\"metadata\"].item()))\n expected = cache_metadata(stage, config)\n if actual != expected:\n raise RuntimeError(f\"sequence cache differs: {actual} != {expected}\")\n values = {\n field: artifact[field].copy()\n for field in SequenceData.__dataclass_fields__\n }\n prepared = SequenceData(**values)\n log(\n f\"loaded full-sequence cache: tokens={len(prepared.sequence_items):,} \"\n f\"targets={prepared.target_count:,}\"\n )\n return prepared\n\n\ndef load_or_prepare(\n public: Path,\n stage: str,\n config: Config,\n cache: Path | None,\n) -> tuple[SequenceData, CandidateSet]:\n candidate_name = (\n \"validation_eligible_candidates.npz\"\n if stage == \"validation\"\n else \"test_eligible_candidates.npz\"\n )\n eligibility = CandidateSet.read(public / \"data\" / candidate_name)\n if cac…10419 tokens truncated… )\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\n )\n ordered = np.argsort(lengths, kind=\"stable\")\n result = torch.zeros(\n len(sequence_rows), config.embedding_dim, dtype=torch.float32\n )\n for group_start in range(0, len(ordered), config.user_batch_size):\n output_rows = ordered[group_start : group_start + config.user_batch_size]\n rows = sequence_rows[output_rows]\n group_lengths = lengths[output_rows]\n hidden = torch.zeros(\n 1, len(output_rows), config.embedding_dim, device=device\n )\n for start in range(0, int(group_lengths.max(initial=0)), config.bptt_steps):\n steps = min(\n config.bptt_steps,\n int(group_lengths.max(initial=0)) - start,\n )\n items = np.full(\n (len(output_rows), steps),\n len(prepared.item_ids),\n dtype=np.uint16,\n )\n features = np.zeros((len(output_rows), steps, 3), dtype=np.float16)\n counts = np.zeros(len(output_rows), dtype=np.int32)\n for local_row, sequence_row in enumerate(rows):\n if sequence_row < 0:\n continue\n count = min(steps, max(int(group_lengths[local_row]) - start, 0))\n if not count:\n continue\n offset = int(prepared.offsets[sequence_row])\n selected = slice(offset + start, offset + start + count)\n items[local_row, :count] = prepared.sequence_items[selected]\n features[local_row, :count] = prepared.event_features[selected]\n counts[local_row] = count\n outputs, _ = event_sequence(\n model,\n torch.from_numpy(items.astype(np.int64)).to(device),\n torch.from_numpy(features.astype(np.float32)).to(device),\n hidden,\n )\n next_rows: list[torch.Tensor] = []\n for local_row, count in enumerate(counts):\n next_rows.append(\n outputs[local_row, count - 1]\n if count\n else hidden[0, local_row]\n )\n hidden = torch.stack(next_rows).unsqueeze(0)\n\n liked_items = torch.from_numpy(\n prepared.inference_liked_items[output_rows].astype(np.int64)\n ).to(device)\n disliked_items = torch.from_numpy(\n prepared.inference_disliked_items[output_rows].astype(np.int64)\n ).to(device)\n queries = user_queries(\n model,\n hidden.squeeze(0),\n model._mean_context_channel(liked_items),\n model._mean_context_channel(disliked_items),\n torch.from_numpy(\n prepared.inference_user_features[output_rows].astype(np.float32)\n ).to(device),\n )\n result[torch.from_numpy(output_rows.astype(np.int64))] = queries.cpu()\n return result\n\n\[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\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n"
}
}
Harness context codex item completed
{
"type": "codex_item_completed",
"item": {
"type": "CommandExecution",
"id": "exec-6e8e241b-7557-451b-b920-3a44b4a5b7d4",
"process_id": "41319",
"command": [
"/bin/sh",
"-lc",
"rg -n '^def |^class ' /task/current_model/train_current_model.py && sed -n '650,1240p' /task/current_model/train_current_model.py"
],
"cwd": "file:///app",
"parsed_cmd": [
{
"type": "search",
"cmd": "rg -n '^def |''^class ' /task/current_model/train_current_model.py",
"query": "^def |^class ",
"path": "train_current_model.py"
},
{
"type": "read",
"cmd": "sed -n '650,1240p' /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": "41:class Config:\n76:class CandidateSet:\n99:class SequenceData:\n127:class TargetPartition:\n140:def log(message: str) -> None:\n144:def seed_everything(seed: int) -> None:\n150:def model_config(config: Config) -> base.TowerConfig:\n174:def append_state_snapshot(\n190:def causal_activity_features(\n247:def build_sequence_parts(\n314:def final_user_features(\n341:def prepare_data(\n471:def cache_metadata(stage: str, config: Config) -> dict[str, object]:\n481:def save_cache(\n493:def load_cache(path: Path, stage: str, config: Config) -> SequenceData:\n511:def load_or_prepare(\n534:def build_item_correction(\n578:def build_known_positive_filter(\n609:def invalid_full_history_negatives(\n630:def draw_full_history_filtered_negatives(\n724:def draw_training_negatives(\n763:def event_sequence(\n774:def pool_explicit_states(\n798:def user_queries(\n813:def candidate_vectors(\n828:def history_windows(\n845:def corrected_negative_loss(\n867:def retained_target_window_starts(\n888:def chronological_target_partition(\n914:def load_organic_like_outcomes(\n955:def build_target_weights(\n1035:def build_dense_block(\n1111:def score_target_microbatches(\n1198:def release_accelerator_cache(device: torch.device) -> None:\n1203:def run_training_epoch(\n1364:def evaluate_validation_bce(\n1488:def snapshot_model(model: nn.Module) -> dict[str, torch.Tensor]:\n1495:def save_epoch_checkpoint(\n1521:def train_model(\n1782:def inference_queries(\n1861:def rank_candidates(\n1912:def write_ranking(frame: pd.DataFrame, output: Path) -> None:\n1922:def main() -> int:\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 liked_items = torch.from_numpy(\n prepared.liked_states[unique_states].astype(np.int64)\n ).to(device)\n disliked_items = torch.from_numpy(\n prepared.disliked_states[unique_states].astype(np.int64)\n ).to(device)\n liked_vectors = model._mean_context_channel(liked_items)\n disliked_vectors = model._mean_context_channel(disliked_items)\n inverse_rows = torch.from_numpy(inverse.astype(np.int64)).to(device)\n counts = prepared.state_counts[state_ids]\n return (\n liked_vectors[inverse_rows],\n disliked_vectors[inverse_rows],\n torch.from_numpy(counts.astype(np.float32)).to(device),\n )\n\n\ndef user_queries(\n model: base.SequentialTwoTower,\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 history_training = totals - validation\n training = history_training.copy()\n if max_train_targets_per_user:\n training = np.minimum(training, max_train_targets_per_user)\n window_starts = retained_target_window_starts(totals, validation, training)\n return TargetPartition(\n window_starts=window_starts,\n training_counts=training,\n validation_counts=validation,\n history_training_counts=history_training,\n )\n\n\ndef load_organic_like_outcomes(\n public: Path, stage: str, item_ids: np.ndarray\n) -> dict[int, dict[int, np.ndarray]]:\n \"\"\"Load mapped organic-like timestamps from public fitting history.\"\"\"\n prefixes = (\"train\",) if stage == \"validation\" else (\"train\", \"val\")\n frames = [\n pd.read_parquet(\n public / \"data\" / f\"{prefix}_likes.parquet\",\n columns=[\"uid\", \"timestamp\", \"item_id\", \"is_organic\"],\n )\n for prefix in prefixes\n ]\n likes = pd.concat(frames, ignore_index=True)\n likes = likes[likes.is_organic == 1].copy()\n mapped = pd.Index(item_ids).get_indexer(\n likes.item_id.to_numpy(dtype=np.int64)\n )\n likes = likes.loc[mapped >= 0, [\"uid\", \"timestamp\"]].copy()\n likes[\"item\"] = mapped[mapped >= 0].astype(np.int32, copy=False)\n likes = likes.sort_values([\"uid\", \"item\", \"timestamp\"], kind=\"stable\")\n nested: dict[int, dict[int, list[int]]] = {}\n for raw_uid, raw_timestamp, raw_item in likes.itertuples(\n index=False, name=None\n ):\n nested.setdefault(int(raw_uid), {}).setdefault(int(raw_item), []).append(\n int(raw_timestamp)\n )\n result = {\n uid: {\n item: np.asarray(timestamps, dtype=np.int64)\n for item, timestamps in items.items()\n }\n for uid, items in nested.items()\n }\n log(\n f\"loaded organic-like outcomes: rows={len(likes):,} \"\n f\"users={len(result):,}\"\n )\n return result\n\n\ndef build_target_weights(\n public: Path,\n stage: str,\n prepared: SequenceData,\n training_counts: np.ndarray,\n config: Config,\n) -> np.ndarray:\n \"\"\"Build nonzero per-token weights without dropping adjacent targets.\"\"\"\n if config.target_weighting == \"uniform\":\n weights = np.ones(len(prepared.sequence_items), dtype=np.float32)\n else:\n weights = np.full(\n len(prepared.sequence_items),\n config.ordinary_target_weight,\n dtype=np.float32,\n )\n organic_strong = (\n (prepared.event_features[:, 0] > 0.5)\n & (prepared.event_features[:, 1] >= 0.8)\n )\n weights[organic_strong] = config.organic_strong_target_weight\n if config.target_weighting == \"discovery-graded\":\n like_outcomes = load_organic_like_outcomes(\n public, stage, prepared.item_ids\n )\n history_end = (\n base.TRAIN_END if stage == \"validation\" else base.VALIDATION_END\n )\n horizon = config.discovery_horizon_days * base.DAY\n grade_three = 0\n for user_row, uid in enumerate(prepared.user_ids):\n start = int(prepared.offsets[user_row])\n end = int(prepared.offsets[user_row + 1])\n if end - start < 2:\n continue\n last_training_target = start + int(training_counts[user_row])\n first_validation_target = last_training_target + 1\n split_timestamp = (\n int(prepared.sequence_timestamps[first_validation_target])\n if first_validation_target < end\n else history_end\n )\n user_likes = like_outcomes.get(int(uid), {})\n seen = {int(prepared.sequence_items[start])}\n for position in range(start + 1, end):\n item = int(prepared.sequence_items[position])\n novel = item not in seen\n seen.add(item)\n if not novel:\n # Repeats remain training examples, but they cannot be\n # eligible discovery outcomes at the corresponding\n # cutoff and therefore keep only the ordinary weight.\n weights[position] = config.ordinary_target_weight\n continue\n timestamps = user_likes.get(item)\n if timestamps is None:\n continue\n timestamp = int(prepared.sequence_timestamps[position])\n upper = min(\n timestamp + horizon,\n split_timestamp\n if position <= last_training_target\n else history_end,\n )\n outcome = int(np.searchsorted(timestamps, timestamp, side=\"left\"))\n if outcome < len(timestamps) and int(timestamps[outcome]) < upper:\n weights[position] = config.organic_like_target_weight\n grade_three += 1\n log(\n f\"grade-3 first-encounter targets={grade_three:,} \"\n f\"horizon_days={config.discovery_horizon_days}\"\n )\n log(\n f\"target weighting={config.target_weighting} \"\n f\"min={weights.min():.3f} mean={weights.mean():.3f} \"\n f\"max={weights.max():.3f}\"\n )\n return weights\n\n\ndef build_dense_block(\n prepared: SequenceData,\n target_weights: np.ndarray,\n users: np.ndarray,\n window_starts: np.ndarray,\n process_counts: np.ndarray,\n score_starts: np.ndarray,\n start: int,\n steps: int,\n config: Config,\n) -> tuple[np.ndarray, ...]:\n \"\"\"Build one causal sequence block and mark the target rows to score.\"\"\"\n batch_size = len(users)\n padding_item = len(prepared.item_ids)\n input_items = np.full((batch_size, steps), padding_item, dtype=np.uint16)\n event_features = np.zeros((batch_size, steps, 3), dtype=np.float16)\n activity_features = np.zeros((batch_size, steps, 4), dtype=np.float16)\n state_ids = np.zeros((batch_size, steps), dtype=np.uint32)\n targets = np.zeros((batch_size, steps), dtype=np.uint16)\n weights = np.ones((batch_size, steps), dtype=np.float32)\n scored = np.zeros((batch_size, steps), dtype=bool)\n histories = np.full(\n (batch_size, steps, config.bptt_steps),\n padding_item,\n dtype=np.uint16,\n )\n processed = np.zeros(batch_size, dtype=np.int32)\n\n lengths = prepared.lengths\n for row, user_row in enumerate(users):\n count = min(steps, max(int(process_counts[row]) - start, 0))\n if not count:\n continue\n offset = int(prepared.offsets[user_row])\n window_start = int(window_starts[row])\n absolute_start = window_start + start\n positions = slice(\n offset + absolute_start,\n offset + absolute_start + count,\n )\n target_positions = slice(\n offset + absolute_start + 1,\n offset + absolute_start + count + 1,\n )\n input_items[row, :count] = prepared.sequence_items[positions]\n event_features[row, :count] = prepared.event_features[positions]\n activity_features[row, :count] = prepared.activity_features[positions]\n state_ids[row, :count] = prepared.state_ids[positions]\n targets[row, :count] = prepared.sequence_items[target_positions]\n weights[row, :count] = target_weights[target_positions]\n score_from = min(count, max(int(score_starts[row]) - start, 0))\n scored[row, score_from:count] = True\n processed[row] = count\n user_sequence = prepared.sequence_items[\n offset : offset + int(lengths[user_row])\n ]\n histories[row, :count] = history_windows(\n user_sequence,\n absolute_start,\n count,\n config.bptt_steps,\n padding_item,\n )\n return (\n input_items,\n event_features,\n activity_features,\n state_ids,\n targets,\n scored,\n histories,\n processed,\n weights,\n )\n\n\ndef score_target_microbatches(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n config: Config,\n sequence_vectors: torch.Tensor,\n flat_states: np.ndarray,\n flat_activity: np.ndarray,\n positive_items: torch.Tensor,\n negatives: torch.Tensor,\n negative_weights: torch.Tensor,\n sample_weights: np.ndarray,\n device: torch.device,\n backward: bool,\n) -> tuple[dict[str, float], torch.Tensor | None]:\n \"\"\"Compute one logical block's exact BCE in bounded-memory slices.\"\"\"\n target_count = len(positive_items)\n total_weight = float(sample_weights.sum())\n if total_weight <= 0:\n raise RuntimeError(\"target block has no positive sample weight\")\n sequence_gradient = torch.zeros_like(sequence_vectors) if backward else None\n statistics = {\n \"loss_sum\": 0.0,\n \"positive_sum\": 0.0,\n \"negative_sum\": 0.0,\n \"pair_correct\": 0.0,\n \"weight_sum\": 0.0,\n }\n for begin in range(0, target_count, config.loss_microbatch_size):\n end = min(begin + config.loss_microbatch_size, target_count)\n selected_states = flat_states[begin:end]\n if backward:\n sequence_input = (\n sequence_vectors[begin:end].detach().requires_grad_(True)\n )\n else:\n sequence_input = sequence_vectors[begin:end]\n liked_vectors, disliked_vectors, _ = pool_explicit_states(\n model, prepared, selected_states, device\n )\n counts = prepared.state_counts[selected_states]\n numeric = np.zeros((end - begin, 6), dtype=np.float32)\n numeric[:, :4] = flat_activity[begin:end]\n numeric[:, 4] = np.log1p(counts[:, 0]) / 6.0\n numeric[:, 5] = np.log1p(counts[:, 1]) / 4.0\n query_vectors = user_queries(\n model,\n sequence_input,\n liked_vectors,\n disliked_vectors,\n torch.from_numpy(numeric).to(device),\n )\n positive_vectors, negative_vectors = candidate_vectors(\n model,\n positive_items[begin:end],\n negatives[begin:end],\n )\n positive_logits = (query_vectors * positive_vectors).sum(dim=1)\n negative_logits = torch.einsum(\n \"bd,bkd->bk\", query_vectors, negative_vectors\n )\n positive_loss = F.softplus(-positive_logits)\n negative_loss = corrected_negative_loss(\n negative_logits, negative_weights[begin:end], config\n )\n selected_weights = torch.from_numpy(\n sample_weights[begin:end].astype(np.float32, copy=False)\n ).to(device)\n loss_sum = (\n selected_weights * (positive_loss + negative_loss) / 2.0\n ).sum()\n if not bool(torch.isfinite(loss_sum)):\n raise RuntimeError(\"dense next-item training produced non-finite loss\")\n if backward:\n (loss_sum / total_weight).backward()\n if sequence_input.grad is None:\n raise RuntimeError(\"sequence microbatch did not produce a gradient\")\n sequence_gradient[begin:end] = sequence_input.grad\n statistics[\"loss_sum\"] += float(loss_sum.detach().cpu())\n statistics[\"positive_sum\"] += float(positive_logits.detach().sum().cpu())\n statistics[\"negative_sum\"] += float(negative_logits.detach().sum().cpu())\n statistics[\"pair_correct\"] += float(\n (positive_logits.detach()[:, None] > negative_logits.detach()).sum().cpu()\n )\n statistics[\"weight_sum\"] += float(selected_weights.sum().detach().cpu())\n return statistics, sequence_gradient\n\n\ndef release_accelerator_cache(device: torch.device) -> None:\n if device.type == \"mps\":\n torch.mps.empty_cache()\n\n\ndef run_training_epoch(\n model: base.SequentialTwoTower,\n optimizer: torch.optim.Optimizer,\n prepared: SequenceData,\n config: Config,\n target_weights: np.ndarray,\n window_starts: np.ndarray,\n target_counts: np.ndarray,\n item_correction: torch.Tensor,\n known_positive_packed: np.ndarray | None,\n generator: torch.Generator,\n device: torch.device,\n epoch: int,\n) -> dict[str, float]:\n model.train()\n train_users = np.flatnonzero(target_counts > 0)\n sorted_users = train_users[\n np.argsort(target_counts[train_users], kind=\"stable\")\n ]\n groups = [\n sorted_users[start : start + config.user_batch_size]\n for start in range(0, len(sorted_users), config.user_batch_size)\n ]\n group_order = torch.randperm(len(groups), generator=generator).numpy()\n negative_count = config.uniform_negatives + config.inbatch_negatives\n total_loss = 0.0\n total_targets = 0\n total_weight = 0.0\n total_positive = 0.0\n total_negative = 0.0\n total_pair_correct = 0.0\n batches = 0\n started = time.monotonic()\n\n for group_number in group_order:\n users = groups[int(group_number)]\n group_counts = target_counts[users]\n hidden = torch.zeros(\n",
"stderr": "",
"aggregated_output": "41:class Config:\n76:class CandidateSet:\n99:class SequenceData:\n127:class TargetPartition:\n140:def log(message: str) -> None:\n144:def seed_everything(seed: int) -> None:\n150:def model_config(config: Config) -> base.TowerConfig:\n174:def append_state_snapshot(\n190:def causal_activity_features(\n247:def build_sequence_parts(\n314:def final_user_features(\n341:def prepare_data(\n471:def cache_metadata(stage: str, config: Config) -> dict[str, object]:\n481:def save_cache(\n493:def load_cache(path: Path, stage: str, config: Config) -> SequenceData:\n511:def load_or_prepare(\n534:def build_item_correction(\n578:def build_known_positive_filter(\n609:def invalid_full_history_negatives(\n630:def draw_full_history_filtered_negatives(\n724:def draw_training_negatives(\n763:def event_sequence(\n774:def pool_explicit_states(\n798:def user_queries(\n813:def candidate_vectors(\n828:def history_windows(\n845:def corrected_negative_loss(\n867:def retained_target_window_starts(\n888:def chronological_target_partition(\n914:def load_organic_like_outcomes(\n955:def build_target_weights(\n1035:def build_dense_block(\n1111:def score_target_microbatches(\n1198:def release_accelerator_cache(device: torch.device) -> None:\n1203:def run_training_epoch(\n1364:def evaluate_validation_bce(\n1488:def snapshot_model(model: nn.Module) -> dict[str, torch.Tensor]:\n1495:def save_epoch_checkpoint(\n1521:def train_model(\n1782:def inference_queries(\n1861:def rank_candidates(\n1912:def write_ranking(frame: pd.DataFrame, output: Path) -> None:\n1922:def main() -> int:\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 liked_items = torch.from_numpy(\n prepared.liked_states[unique_states].astype(np.int64)\n ).to(device)\n disliked_items = torch.from_numpy(\n prepared.disliked_states[unique_states].astype(np.int64)\n ).to(device)\n liked_vectors = model._mean_context_channel(liked_items)\n disliked_vectors = model._mean_context_channel(disliked_items)\n inverse_rows = torch.from_numpy(inverse.astype(np.int64)).to(device)\n counts = prepared.state_counts[state_ids]\n return (\n liked_vectors[inverse_rows],\n disliked_vectors[inverse_rows],\n torch.from_numpy(counts.astype(np.float32)).to(device),\n )\n\n\ndef user_queries(\n model: base.SequentialTwoTower,\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 history_training = totals - validation\n training = history_training.copy()\n if max_train_targets_per_user:\n training = np.minimum(training, max_train_targets_per_user)\n window_starts = retained_target_window_starts(totals, validation, training)\n return TargetPartition(\n window_starts=window_starts,\n training_counts=training,\n validation_counts=validation,\n history_training_counts=history_training,\n )\n\n\ndef load_organic_like_outcomes(\n public: Path, stage: str, item_ids: np.ndarray\n) -> dict[int, dict[int, np.ndarray]]:\n \"\"\"Load mapped organic-like timestamps from public fitting history.\"\"\"\n prefixes = (\"train\",) if stage == \"validation\" else (\"train\", \"val\")\n frames = [\n pd.read_parquet(\n public / \"data\" / f\"{prefix}_likes.parquet\",\n columns=[\"uid\", \"timestamp\", \"item_id\", \"is_organic\"],\n )\n for prefix in prefixes\n ]\n likes = pd.concat(frames, ignore_index=True)\n likes = likes[likes.is_organic == 1].copy()\n mapped = pd.Index(item_ids).get_indexer(\n likes.item_id.to_numpy(dtype=np.int64)\n )\n likes = likes.loc[mapped >= 0, [\"uid\", \"timestamp\"]].copy()\n likes[\"item\"] = mapped[mapped >= 0].astype(np.int32, copy=False)\n likes = likes.sort_values([\"uid\", \"item\", \"timestamp\"], kind=\"stable\")\n nested: dict[int, dict[int, list[int]]] = {}\n for raw_uid, raw_timestamp, raw_item in likes.itertuples(\n index=False, name=None\n ):\n nested.setdefault(int(raw_uid), {}).setdefault(int(raw_item), []).append(\n int(raw_timestamp)\n )\n result = {\n uid: {\n item: np.asarray(timestamps, dtype=np.int64)\n for item, timestamps in items.items()\n }\n for uid, items in nested.items()\n }\n log(\n f\"loaded organic-like outcomes: rows={len(likes):,} \"\n f\"users={len(result):,}\"\n )\n return result\n\n\ndef build_target_weights(\n public: Path,\n stage: str,\n prepared: SequenceData,\n training_counts: np.ndarray,\n config: Config,\n) -> np.ndarray:\n \"\"\"Build nonzero per-token weights without dropping adjacent targets.\"\"\"\n if config.target_weighting == \"uniform\":\n weights = np.ones(len(prepared.sequence_items), dtype=np.float32)\n else:\n weights = np.full(\n len(prepared.sequence_items),\n config.ordinary_target_weight,\n dtype=np.float32,\n )\n organic_strong = (\n (prepared.event_features[:, 0] > 0.5)\n & (prepared.event_features[:, 1] >= 0.8)\n )\n weights[organic_strong] = config.organic_strong_target_weight\n if config.target_weighting == \"discovery-graded\":\n like_outcomes = load_organic_like_outcomes(\n public, stage, prepared.item_ids\n )\n history_end = (\n base.TRAIN_END if stage == \"validation\" else base.VALIDATION_END\n )\n horizon = config.discovery_horizon_days * base.DAY\n grade_three = 0\n for user_row, uid in enumerate(prepared.user_ids):\n start = int(prepared.offsets[user_row])\n end = int(prepared.offsets[user_row + 1])\n if end - start < 2:\n continue\n last_training_target = start + int(training_counts[user_row])\n first_validation_target = last_training_target + 1\n split_timestamp = (\n int(prepared.sequence_timestamps[first_validation_target])\n if first_validation_target < end\n else history_end\n )\n user_likes = like_outcomes.get(int(uid), {})\n seen = {int(prepared.sequence_items[start])}\n for position in range(start + 1, end):\n item = int(prepared.sequence_items[position])\n novel = item not in seen\n seen.add(item)\n if not novel:\n # Repeats remain training examples, but they cannot be\n # eligible discovery outcomes at the corresponding\n # cutoff and therefore keep only the ordinary weight.\n weights[position] = config.ordinary_target_weight\n continue\n timestamps = user_likes.get(item)\n if timestamps is None:\n continue\n timestamp = int(prepared.sequence_timestamps[position])\n upper = min(\n timestamp + horizon,\n split_timestamp\n if position <= last_training_target\n else history_end,\n )\n outcome = int(np.searchsorted(timestamps, timestamp, side=\"left\"))\n if outcome < len(timestamps) and int(timestamps[outcome]) < upper:\n weights[position] = config.organic_like_target_weight\n grade_three += 1\n log(\n f\"grade-3 first-encounter targets={grade_three:,} \"\n f\"horizon_days={config.discovery_horizon_days}\"\n )\n log(\n f\"target weighting={config.target_weighting} \"\n f\"min={weights.min():.3f} mean={weights.mean():.3f} \"\n f\"max={weights.max():.3f}\"\n )\n return weights\n\n\ndef build_dense_block(\n prepared: SequenceData,\n target_weights: np.ndarray,\n users: np.ndarray,\n window_starts: np.ndarray,\n process_counts: np.ndarray,\n score_starts: np.ndarray,\n start: int,\n steps: int,\n config: Config,\n) -> tuple[np.ndarray, ...]:\n \"\"\"Build one causal sequence block and mark the target rows to score.\"\"\"\n batch_size = len(users)\n padding_item = len(prepared.item_ids)\n input_items = np.full((batch_size, steps), padding_item, dtype=np.uint16)\n event_features = np.zeros((batch_size, steps, 3), dtype=np.float16)\n activity_features = np.zeros((batch_size, steps, 4), dtype=np.float16)\n state_ids = np.zeros((batch_size, steps), dtype=np.uint32)\n targets = np.zeros((batch_size, steps), dtype=np.uint16)\n weights = np.ones((batch_size, steps), dtype=np.float32)\n scored = np.zeros((batch_size, steps), dtype=bool)\n histories = np.full(\n (batch_size, steps, config.bptt_steps),\n padding_item,\n dtype=np.uint16,\n )\n processed = np.zeros(batch_size, dtype=np.int32)\n\n lengths = prepared.lengths\n for row, user_row in enumerate(users):\n count = min(steps, max(int(process_counts[row]) - start, 0))\n if not count:\n continue\n offset = int(prepared.offsets[user_row])\n window_start = int(window_starts[row])\n absolute_start = window_start + start\n positions = slice(\n offset + absolute_start,\n offset + absolute_start + count,\n )\n target_positions = slice(\n offset + absolute_start + 1,\n offset + absolute_start + count + 1,\n )\n input_items[row, :count] = prepared.sequence_items[positions]\n event_features[row, :count] = prepared.event_features[positions]\n activity_features[row, :count] = prepared.activity_features[positions]\n state_ids[row, :count] = prepared.state_ids[positions]\n targets[row, :count] = prepared.sequence_items[target_positions]\n weights[row, :count] = target_weights[target_positions]\n score_from = min(count, max(int(score_starts[row]) - start, 0))\n scored[row, score_from:count] = True\n processed[row] = count\n user_sequence = prepared.sequence_items[\n offset : offset + int(lengths[user_row])\n ]\n histories[row, :count] = history_windows(\n user_sequence,\n absolute_start,\n count,\n config.bptt_steps,\n padding_item,\n )\n return (\n input_items,\n event_features,\n activity_features,\n state_ids,\n targets,\n scored,\n histories,\n processed,\n weights,\n )\n\n\ndef score_target_microbatches(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n config: Config,\n sequence_vectors: torch.Tensor,\n flat_states: np.ndarray,\n flat_activity: np.ndarray,\n positive_items: torch.Tensor,\n negatives: torch.Tensor,\n negative_weights: torch.Tensor,\n sample_weights: np.ndarray,\n device: torch.device,\n backward: bool,\n) -> tuple[dict[str, float], torch.Tensor | None]:\n \"\"\"Compute one logical block's exact BCE in bounded-memory slices.\"\"\"\n target_count = len(positive_items)\n total_weight = float(sample_weights.sum())\n if total_weight <= 0:\n raise RuntimeError(\"target block has no positive sample weight\")\n sequence_gradient = torch.zeros_like(sequence_vectors) if backward else None\n statistics = {\n \"loss_sum\": 0.0,\n \"positive_sum\": 0.0,\n \"negative_sum\": 0.0,\n \"pair_correct\": 0.0,\n \"weight_sum\": 0.0,\n }\n for begin in range(0, target_count, config.loss_microbatch_size):\n end = min(begin + config.loss_microbatch_size, target_count)\n selected_states = flat_states[begin:end]\n if backward:\n sequence_input = (\n sequence_vectors[begin:end].detach().requires_grad_(True)\n )\n else:\n sequence_input = sequence_vectors[begin:end]\n liked_vectors, disliked_vectors, _ = pool_explicit_states(\n model, prepared, selected_states, device\n )\n counts = prepared.state_counts[selected_states]\n numeric = np.zeros((end - begin, 6), dtype=np.float32)\n numeric[:, :4] = flat_activity[begin:end]\n numeric[:, 4] = np.log1p(counts[:, 0]) / 6.0\n numeric[:, 5] = np.log1p(counts[:, 1]) / 4.0\n query_vectors = user_queries(\n model,\n sequence_input,\n liked_vectors,\n disliked_vectors,\n torch.from_numpy(numeric).to(device),\n )\n positive_vectors, negative_vectors = candidate_vectors(\n model,\n positive_items[begin:end],\n negatives[begin:end],\n )\n positive_logits = (query_vectors * positive_vectors).sum(dim=1)\n negative_logits = torch.einsum(\n \"bd,bkd->bk\", query_vectors, negative_vectors\n )\n positive_loss = F.softplus(-positive_logits)\n negative_loss = corrected_negative_loss(\n negative_logits, negative_weights[begin:end], config\n )\n selected_weights = torch.from_numpy(\n sample_weights[begin:end].astype(np.float32, copy=False)\n ).to(device)\n loss_sum = (\n selected_weights * (positive_loss + negative_loss) / 2.0\n ).sum()\n if not bool(torch.isfinite(loss_sum)):\n raise RuntimeError(\"dense next-item training produced non-finite loss\")\n if backward:\n (loss_sum / total_weight).backward()\n if sequence_input.grad is None:\n raise RuntimeError(\"sequence microbatch did not produce a gradient\")\n sequence_gradient[begin:end] = sequence_input.grad\n statistics[\"loss_sum\"] += float(loss_sum.detach().cpu())\n statistics[\"positive_sum\"] += float(positive_logits.detach().sum().cpu())\n statistics[\"negative_sum\"] += float(negative_logits.detach().sum().cpu())\n statistics[\"pair_correct\"] += float(\n (positive_logits.detach()[:, None] > negative_logits.detach()).sum().cpu()\n )\n statistics[\"weight_sum\"] += float(selected_weights.sum().detach().cpu())\n return statistics, sequence_gradient\n\n\ndef release_accelerator_cache(device: torch.device) -> None:\n if device.type == \"mps\":\n torch.mps.empty_cache()\n\n\ndef run_training_epoch(\n model: base.SequentialTwoTower,\n optimizer: torch.optim.Optimizer,\n prepared: SequenceData,\n config: Config,\n target_weights: np.ndarray,\n window_starts: np.ndarray,\n target_counts: np.ndarray,\n item_correction: torch.Tensor,\n known_positive_packed: np.ndarray | None,\n generator: torch.Generator,\n device: torch.device,\n epoch: int,\n) -> dict[str, float]:\n model.train()\n train_users = np.flatnonzero(target_counts > 0)\n sorted_users = train_users[\n np.argsort(target_counts[train_users], kind=\"stable\")\n ]\n groups = [\n sorted_users[start : start + config.user_batch_size]\n for start in range(0, len(sorted_users), config.user_batch_size)\n ]\n group_order = torch.randperm(len(groups), generator=generator).numpy()\n negative_count = config.uniform_negatives + config.inbatch_negatives\n total_loss = 0.0\n total_targets = 0\n total_weight = 0.0\n total_positive = 0.0\n total_negative = 0.0\n total_pair_correct = 0.0\n batches = 0\n started = time.monotonic()\n\n for group_number in group_order:\n users = groups[int(group_number)]\n group_counts = target_counts[users]\n hidden = torch.zeros(\n",
"exit_code": 0,
"duration": {
"secs": 0,
"nanos": 4330
},
"formatted_output": "41:class Config:\n76:class CandidateSet:\n99:class SequenceData:\n127:class TargetPartition:\n140:def log(message: str) -> None:\n144:def seed_everything(seed: int) -> None:\n150:def model_config(config: Config) -> base.TowerConfig:\n174:def append_state_snapshot(\n190:def causal_activity_features(\n247:def build_sequence_parts(\n314:def final_user_features(\n341:def prepare_data(\n471:def cache_metadata(stage: str, config: Config) -> dict[str, object]:\n481:def save_cache(\n493:def load_cache(path: Path, stage: str, config: Config) -> SequenceData:\n511:def load_or_prepare(\n534:def build_item_correction(\n578:def build_known_positive_filter(\n609:def invalid_full_history_negatives(\n630:def draw_full_history_filtered_negatives(\n724:def draw_training_negatives(\n763:def event_sequence(\n774:def pool_explicit_states(\n798:def user_queries(\n813:def candidate_vectors(\n828:def history_windows(\n845:def corrected_negative_loss(\n867:def retained_target_window_starts(\n888:def chronological_target_partition(\n914:def load_organic_like_outcomes(\n955:def build_target_weights(\n1035:def build_dense_block(\n1111:def score_target_microbatches(\n1198:def release_accelerator_cache(device: torch.device) -> None:\n1203:def run_training_epoch(\n1364:def evaluate_validation_bce(\n1488:def snapshot_model(model: nn.Module) -> dict[str, torch.Tensor]:\n1495:def save_epoch_checkpoint(\n1521:def train_model(\n1782:def inference_queries(\n1861:def rank_candidates(\n1912:def write_ranking(frame: pd.DataFrame, output: Path) -> None:\n1922:def main() -> int:\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 liked_items = torch.from_numpy(\n prepared.liked_states[unique_states].astype(np.int64)\n ).to(device)\n disliked_items = torch.from_numpy(\n prepared.disliked_states[unique_states].astype(np.int64)\n ).to(device)\n liked_vectors = model._mean_context_channel(liked_items)\n disliked_vectors = model._mean_context_channel(disliked_items)\n inverse_rows = torch.from_numpy(inverse.astype(np.int64)).to(device)\n counts = prepared.state_counts[state_ids]\n return (\n liked_vectors[inverse_rows],\n disliked_vectors[inverse_rows],\n torch.from_numpy(counts.astype(np.float32)).to(device),\n )\n\n\ndef user_queries(\n model: base.SequentialTwoTower,\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 history_training = totals - validation\n training = history_training.copy()\n if max_train_targets_per_user:\n training = np.minimum(training, max_train_targets_per_user)\n window_starts = retained_target_window_starts(totals, validation, training)\n return TargetPartition(\n window_starts=window_starts,\n training_counts=training,\n validation_counts=validation,\n history_training_counts=history_training,\n )\n\n\ndef load_organic_like_outcomes(\n public: Path, stage: str, item_ids: np.ndarray\n) -> dict[int, dict[int, np.ndarray]]:\n \"\"\"Load mapped organic-like timestamps from public fitting history.\"\"\"\n prefixes = (\"train\",) if stage == \"validation\" else (\"train\", \"val\")\n frames = [\n pd.read_parquet(\n public / \"data\" / f\"{prefix}_likes.parquet\",\n columns=[\"uid\", \"timestamp\", \"item_id\", \"is_organic\"],\n )\n for prefix in prefixes\n ]\n likes = pd.concat(frames, ignore_index=True)\n likes = likes[likes.is_organic == 1].copy()\n mapped = pd.Index(item_ids).get_indexer(\n likes.item_id.to_numpy(dtype=np.int64)\n )\n likes = likes.loc[mapped >= 0, [\"uid\", \"timestamp\"]].copy()\n likes[\"item\"] = mapped[mapped >= 0].astype(np.int32, copy=False)\n likes = likes.sort_values([\"uid\", \"item\", \"timestamp\"], kind=\"stable\")\n nested: dict[int, dict[int, list[int]]] = {}\n for raw_uid, raw_timestamp, raw_item in likes.itertuples(\n index=False, name=None\n ):\n nested.setdefault(int(raw_uid), {}).setdefault(int(raw_item), []).append(\n int(raw_timestamp)\n )\n result = {\n uid: {\n item: np.asarray(timestamps, dtype=np.int64)\n for item, timestamps in items.items()\n }\n for uid, items in nested.items()\n }\n log(\n f\"loaded organic-like outcomes: rows={len(likes):,} \"\n f\"users={len(result):,}\"\n )\n return result\n\n\ndef build_target_weights(\n public: Path,\n stage: str,\n prepared: SequenceData,\n training_counts: np.ndarray,\n config: Config,\n) -> np.ndarray:\n \"\"\"Build nonzero per-token weights without dropping adjacent targets.\"\"\"\n if config.target_weighting == \"uniform\":\n weights = np.ones(len(prepared.sequence_items), dtype=np.float32)\n else:\n weights = np.full(\n len(prepared.sequence_items),\n config.ordinary_target_weight,\n dtype=np.float32,\n )\n organic_strong = (\n (prepared.event_features[:, 0] > 0.5)\n & (prepared.event_features[:, 1] >= 0.8)\n )\n weights[organic_strong] = config.organic_strong_target_weight\n if config.target_weighting == \"discovery-graded\":\n like_outcomes = load_organic_like_outcomes(\n public, stage, prepared.item_ids\n )\n history_end = (\n base.TRAIN_END if stage == \"validation\" else base.VALIDATION_END\n )\n horizon = config.discovery_horizon_days * base.DAY\n grade_three = 0\n for user_row, uid in enumerate(prepared.user_ids):\n start = int(prepared.offsets[user_row])\n end = int(prepared.offsets[user_row + 1])\n if end - start < 2:\n continue\n last_training_target = start + int(training_counts[user_row])\n first_validation_target = last_training_target + 1\n split_timestamp = (\n int(prepared.sequence_timestamps[first_validation_target])\n if first_validation_target < end\n else history_end\n )\n user_likes = like_outcomes.get(int(uid), {})\n seen = {int(prepared.sequence_items[start])}\n for position in range(start + 1, end):\n item = int(prepared.sequence_items[position])\n novel = item not in seen\n seen.add(item)\n if not novel:\n # Repeats remain training examples, but they cannot be\n # eligible discovery outcomes at the corresponding\n # cutoff and therefore keep only the ordinary weight.\n weights[position] = config.ordinary_target_weight\n continue\n timestamps = user_likes.get(item)\n if timestamps is None:\n continue\n timestamp = int(prepared.sequence_timestamps[position])\n upper = min(\n timestamp + horizon,\n split_timestamp\n if position <= last_training_target\n else history_end,\n )\n outcome = int(np.searchsorted(timestamps, timestamp, side=\"left\"))\n if outcome < len(timestamps) and int(timestamps[outcome]) < upper:\n weights[position] = config.organic_like_target_weight\n grade_three += 1\n log(\n f\"grade-3 first-encounter targets={grade_three:,} \"\n f\"horizon_days={config.discovery_horizon_days}\"\n )\n log(\n f\"target weighting={config.target_weighting} \"\n f\"min={weights.min():.3f} mean={weights.mean():.3f} \"\n f\"max={weights.max():.3f}\"\n )\n return weights\n\n\ndef build_dense_block(\n prepared: SequenceData,\n target_weights: np.ndarray,\n users: np.ndarray,\n window_starts: np.ndarray,\n process_counts: np.ndarray,\n score_starts: np.ndarray,\n start: int,\n steps: int,\n config: Config,\n) -> tuple[np.ndarray, ...]:\n \"\"\"Build one causal sequence block and mark the target rows to score.\"\"\"\n batch_size = len(users)\n padding_item = len(prepared.item_ids)\n input_items = np.full((batch_size, steps), padding_item, dtype=np.uint16)\n event_features = np.zeros((batch_size, steps, 3), dtype=np.float16)\n activity_features = np.zeros((batch_size, steps, 4), dtype=np.float16)\n state_ids = np.zeros((batch_size, steps), dtype=np.uint32)\n targets = np.zeros((batch_size, steps), dtype=np.uint16)\n weights = np.ones((batch_size, steps), dtype=np.float32)\n scored = np.zeros((batch_size, steps), dtype=bool)\n histories = np.full(\n (batch_size, steps, config.bptt_steps),\n padding_item,\n dtype=np.uint16,\n )\n processed = np.zeros(batch_size, dtype=np.int32)\n\n lengths = prepared.lengths\n for row, user_row in enumerate(users):\n count = min(steps, max(int(process_counts[row]) - start, 0))\n if not count:\n continue\n offset = int(prepared.offsets[user_row])\n window_start = int(window_starts[row])\n absolute_start = window_start + start\n positions = slice(\n offset + absolute_start,\n offset + absolute_start + count,\n )\n target_positions = slice(\n offset + absolute_start + 1,\n offset + absolute_start + count + 1,\n )\n input_items[row, :count] = prepared.sequence_items[positions]\n event_features[row, :count] = prepared.event_features[positions]\n activity_features[row, :count] = prepared.activity_features[positions]\n state_ids[row, :count] = prepared.state_ids[positions]\n targets[row, :count] = prepared.sequence_items[target_positions]\n weights[row, :count] = target_weights[target_positions]\n score_from = min(count, max(int(score_starts[row]) - start, 0))\n scored[row, score_from:count] = True\n processed[row] = count\n user_sequence = prepared.sequence_items[\n offset : offset + int(lengths[user_row])\n ]\n histories[row, :count] = history_windows(\n user_sequence,\n absolute_start,\n count,\n config.bptt_steps,\n padding_item,\n )\n return (\n input_items,\n event_features,\n activity_features,\n state_ids,\n targets,\n scored,\n histories,\n processed,\n weights,\n )\n\n\ndef score_target_microbatches(\n model: base.SequentialTwoTower,\n prepared: SequenceData,\n config: Config,\n sequence_vectors: torch.Tensor,\n flat_states: np.ndarray,\n flat_activity: np.ndarray,\n positive_items: torch.Tensor,\n negatives: torch.Tensor,\n negative_weights: torch.Tensor,\n sample_weights: np.ndarray,\n device: torch.device,\n backward: bool,\n) -> tuple[dict[str, float], torch.Tensor | None]:\n \"\"\"Compute one logical block's exact BCE in bounded-memory slices.\"\"\"\n target_count = len(positive_items)\n total_weight = float(sample_weights.sum())\n if total_weight <= 0:\n raise RuntimeError(\"target block has no positive sample weight\")\n sequence_gradient = torch.zeros_like(sequence_vectors) if backward else None\n statistics = {\n \"loss_sum\": 0.0,\n \"positive_sum\": 0.0,\n \"negative_sum\": 0.0,\n \"pair_correct\": 0.0,\n \"weight_sum\": 0.0,\n }\n for begin in range(0, target_count, config.loss_microbatch_size):\n end = min(begin + config.loss_microbatch_size, target_count)\n selected_states = flat_states[begin:end]\n if backward:\n sequence_input = (\n sequence_vectors[begin:end].detach().requires_grad_(True)\n )\n else:\n sequence_input = sequence_vectors[begin:end]\n liked_vectors, disliked_vectors, _ = pool_explicit_states(\n model, prepared, selected_states, device\n )\n counts = prepared.state_counts[selected_states]\n numeric = np.zeros((end - begin, 6), dtype=np.float32)\n numeric[:, :4] = flat_activity[begin:end]\n numeric[:, 4] = np.log1p(counts[:, 0]) / 6.0\n numeric[:, 5] = np.log1p(counts[:, 1]) / 4.0\n query_vectors = user_queries(\n model,\n sequence_input,\n liked_vectors,\n disliked_vectors,\n torch.from_numpy(numeric).to(device),\n )\n positive_vectors, negative_vectors = candidate_vectors(\n model,\n positive_items[begin:end],\n negatives[begin:end],\n )\n positive_logits = (query_vectors * positive_vectors).sum(dim=1)\n negative_logits = torch.einsum(\n \"bd,bkd->bk\", query_vectors, negative_vectors\n )\n positive_loss = F.softplus(-positive_logits)\n negative_loss = corrected_negative_loss(\n negative_logits, negative_weights[begin:end], config\n )\n selected_weights = torch.from_numpy(\n sample_weights[begin:end].astype(np.float32, copy=False)\n ).to(device)\n loss_sum = (\n selected_weights * (positive_loss + negative_loss) / 2.0\n ).sum()\n if not bool(torch.isfinite(loss_sum)):\n raise RuntimeError(\"dense next-item training produced non-finite loss\")\n if backward:\n (loss_sum / total_weight).backward()\n if sequence_input.grad is None:\n raise RuntimeError(\"sequence microbatch did not produce a gradient\")\n sequence_gradient[begin:end] = sequence_input.grad\n statistics[\"loss_sum\"] += float(loss_sum.detach().cpu())\n statistics[\"positive_sum\"] += float(positive_logits.detach().sum().cpu())\n statistics[\"negative_sum\"] += float(negative_logits.detach().sum().cpu())\n statistics[\"pair_correct\"] += float(\n (positive_logits.detach()[:, None] > negative_logits.detach()).sum().cpu()\n )\n statistics[\"weight_sum\"] += float(selected_weights.sum().detach().cpu())\n return statistics, sequence_gradient\n\n\ndef release_accelerator_cache(device: torch.device) -> None:\n if device.type == \"mps\":\n torch.mps.empty_cache()\n\n\ndef run_training_epoch(\n model: base.SequentialTwoTower,\n optimizer: torch.optim.Optimizer,\n prepared: SequenceData,\n config: Config,\n target_weights: np.ndarray,\n window_starts: np.ndarray,\n target_counts: np.ndarray,\n item_correction: torch.Tensor,\n known_positive_packed: np.ndarray | None,\n generator: torch.Generator,\n device: torch.device,\n epoch: int,\n) -> dict[str, float]:\n model.train()\n train_users = np.flatnonzero(target_counts > 0)\n sorted_users = train_users[\n np.argsort(target_counts[train_users], kind=\"stable\")\n ]\n groups = [\n sorted_users[start : start + config.user_batch_size]\n for start in range(0, len(sorted_users), config.user_batch_size)\n ]\n group_order = torch.randperm(len(groups), generator=generator).numpy()\n negative_count = config.uniform_negatives + config.inbatch_negatives\n total_loss = 0.0\n total_targets = 0\n total_weight = 0.0\n total_positive = 0.0\n total_negative = 0.0\n total_pair_correct = 0.0\n batches = 0\n started = time.monotonic()\n\n for group_number in group_order:\n users = groups[int(group_number)]\n group_counts = target_counts[users]\n hidden = torch.zeros(\n"
}
}
Harness context codex item completed
{
"type": "codex_item_completed",
"item": {
"type": "CommandExecution",
"id": "exec-81b51fed-7199-41be-b714-6d1c60ed65a7",
"process_id": "69073",
"command": [
"/bin/sh",
"-lc",
"cat /app/solution/baseline.log && which python && ls /usr/local/bin/python* && sed -n '534,650p' /task/current_model/train_current_model.py && sed -n '1240,1364p' /task/current_model/train_current_model.py && sed -n '1521,1650p' /task/current_model/train_current_model.py && cat /task/evaluate_public.py"
],
"cwd": "file:///app",
"parsed_cmd": [
{
"type": "unknown",
"cmd": "cat /app/solution/baseline.log && which python && ls /usr/local/bin/python* && sed -n '534,650p' /task/current_model/train_current_model.py && sed -n '1240,1364p' /task/current_model/train_current_model.py && sed -n '1521,1650p' /task/current_model/train_current_model.py && cat /task/evaluate_public.py"
}
],
"source": "unified_exec_startup",
"status": "completed",
"stdout": "Traceback (most recent call last):\n File \"/task/current_model/train_current_model.py\", line 28, in <module>\n import numpy as np\nModuleNotFoundError: No module named 'numpy'\n/usr/local/bin/python\n/usr/local/bin/python\n/usr/local/bin/python-config\n/usr/local/bin/python3\n/usr/local/bin/python3-config\n/usr/local/bin/python3.12\n/usr/local/bin/python3.12-config\ndef build_item_correction(\n prepared: SequenceData,\n config: Config,\n target_starts: np.ndarray,\n target_counts: np.ndarray,\n) -> torch.Tensor:\n \"\"\"Estimate in-batch propensities from the selected training targets.\"\"\"\n counts = np.zeros(len(prepared.item_ids), dtype=np.float64)\n for user_row, count in enumerate(target_counts):\n if not count:\n continue\n offset = int(prepared.offsets[user_row])\n target_start = int(target_starts[user_row])\n counts += np.bincount(\n prepared.sequence_items[\n offset\n + target_start\n + 1 : offset\n + target_start\n + int(count)\n + 1\n ].astype(np.int64),\n minlength=len(prepared.item_ids),\n )\n total = float(counts.sum())\n probabilities = (counts + 1.0) / (total + len(counts))\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 hidden = torch.zeros(\n 1, len(users), config.embedding_dim, device=device\n )\n max_targets = int(group_counts.max())\n for start in range(0, max_targets, config.bptt_steps):\n steps = min(config.bptt_steps, max_targets - start)\n block = build_dense_block(\n prepared,\n target_weights,\n users,\n window_starts[users],\n group_counts,\n np.zeros(len(users), dtype=np.int64),\n start,\n steps,\n config,\n )\n sequence_outputs, _ = event_sequence(\n model,\n torch.from_numpy(block[0].astype(np.int64)).to(device),\n torch.from_numpy(block[1].astype(np.float32)).to(device),\n hidden,\n )\n hidden = torch.stack(\n [\n sequence_outputs[row, count - 1]\n if count\n else hidden[0, row]\n for row, count in enumerate(block[7])\n ]\n ).unsqueeze(0).detach()\n flat_scored = block[5].reshape(-1)\n if not np.any(flat_scored):\n continue\n mask = torch.from_numpy(flat_scored).to(device)\n sequence_vectors = sequence_outputs.reshape(\n -1, config.embedding_dim\n )[mask]\n flat_states = block[3].reshape(-1)[flat_scored]\n positive_items = torch.from_numpy(\n block[4].reshape(-1)[flat_scored].astype(np.int64)\n ).to(device)\n known_history = torch.from_numpy(\n block[6].reshape(-1, config.bptt_steps)[flat_scored].astype(\n np.int64\n )\n ).to(device)\n liked_items = torch.from_numpy(\n prepared.liked_states[flat_states].astype(np.int64)\n ).to(device)\n flat_user_rows = np.broadcast_to(\n users[:, None], block[5].shape\n ).reshape(-1)[flat_scored]\n negatives, negative_weights = draw_training_negatives(\n positive_items,\n known_history,\n liked_items,\n flat_user_rows,\n known_positive_packed,\n prepared,\n config,\n item_correction,\n generator,\n device,\n )\n optimizer.zero_grad(set_to_none=True)\n statistics, sequence_gradient = score_target_microbatches(\n model,\n prepared,\n config,\n sequence_vectors,\n flat_states,\n block[2].reshape(-1, 4)[flat_scored].astype(np.float32),\n positive_items,\n negatives,\n negative_weights,\n block[8].reshape(-1)[flat_scored],\n device,\n backward=True,\n )\n if sequence_gradient is None:\n raise RuntimeError(\"training block omitted its sequence gradient\")\n sequence_vectors.backward(sequence_gradient)\n nn.utils.clip_grad_norm_(model.parameters(), config.gradient_clip)\n optimizer.step()\n\n count = len(positive_items)\n total_loss += statistics[\"loss_sum\"]\n total_targets += count\n total_weight += statistics[\"weight_sum\"]\n total_positive += statistics[\"positive_sum\"]\n total_negative += statistics[\"negative_sum\"]\n total_pair_correct += statistics[\"pair_correct\"]\n batches += 1\n if batches % 25 == 0:\n release_accelerator_cache(device)\n if batches % 250 == 0:\n batch_bce = statistics[\"loss_sum\"] / max(\n statistics[\"weight_sum\"], 1.0e-12\n )\n running_bce = total_loss / max(total_weight, 1.0e-12)\n log(\n f\"epoch={epoch}/{config.epochs} batches={batches:,} \"\n f\"targets={total_targets:,} \"\n f\"batch_bce={batch_bce:.6f} \"\n f\"running_bce={running_bce:.6f} \"\n f\"elapsed={time.monotonic() - started:.1f}s\"\n )\n\n return {\n \"epoch\": float(epoch),\n \"loss\": total_loss / max(total_weight, 1.0e-12),\n \"positive_logit\": total_positive / max(total_targets, 1),\n \"negative_logit\": total_negative / max(total_targets * negative_count, 1),\n \"pair_accuracy\": total_pair_correct\n / max(total_targets * negative_count, 1),\n \"targets\": float(total_targets),\n \"target_weight_sum\": total_weight,\n \"batches\": float(batches),\n \"seconds\": time.monotonic() - started,\n }\n\n\[email protected]_grad()\ndef evaluate_validation_bce(\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#!/usr/bin/env python3\n\"\"\"Score a validation-stage ranking against public discovery labels.\n\n``build_task.py`` publishes this module with the solver-visible task files. It\nloads target users, validation eligibility, and validation-window events from\nthe adjacent ``data`` directory, then evaluates one ranking supplied on the\ncommand line. Stage-independent mechanics come from the adjacent public\n``ranking_evaluation.py`` module. The builder installs the same checked-in\nmodule in the private judge, so public and private scoring mechanics cannot\ndrift. A relevant candidate receives grade 3 for an organic like or grade 1 for\nan organic listen with at least 80 percent completion; a like takes precedence\nwhen both events exist.\n\nBefore scoring, the ranking must contain exactly 100 distinct eligible items\nfor every target user, with integer-valued ``uid``, ``item_id``, and ``rank``\ncolumns and ranks 1 through 100 exactly once per user. Extra columns are\nignored. Quality is graded NDCG@10, averaged only over users who have at least\none eligible relevant item in the validation window.\n\nThe evaluator prints a JSON object containing the model path and scores. It\ndoes not apply the task's pass threshold; its purpose is model development and\nselection. The private judge applies the threshold to the final submission.\n\nUsage::\n\n python evaluate_public.py <validation-ranking.parquet|csv>\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport sys\nfrom pathlib import Path\n\nimport pandas as pd\n\nfrom ranking_evaluation import (\n CandidateSet,\n RankingValidationError,\n ranked_items,\n read_ranking,\n score_rankings,\n validate_ranking,\n)\n\n\nHERE = Path(__file__).resolve().parent\nDATA = HERE / \"data\"\nLISTEN_STRONG = 80\n\n\ndef validation_truth(\n eligibility: CandidateSet, targets: set[int]\n) -> dict[int, dict[int, int]]:\n \"\"\"Build cutoff-correct graded relevance from public validation events.\n\n Only target-user events for eligible candidates are retained. Organic\n listens completed to at least 80 percent receive grade 1, and organic likes\n receive grade 3. Repeated user-item events are collapsed, and processing\n likes second ensures that grade 3 wins over grade 1.\n\n Args:\n eligibility: Validation-cutoff candidate membership service.\n targets: Complete set of target user IDs.\n\n Returns:\n A nested ``uid -> item_id -> grade`` mapping. Users without an eligible\n relevant event are omitted.\n \"\"\"\n listens = pd.read_parquet(DATA / \"val_listens.parquet\")\n likes = pd.read_parquet(DATA / \"val_likes.parquet\")\n strong = listens[\n listens.uid.isin(targets)\n & (listens.is_organic == 1)\n & (listens.played_ratio_pct >= LISTEN_STRONG)\n ][[\"uid\", \"item_id\"]].drop_duplicates()\n organic_likes = likes[\n likes.uid.isin(targets) & (likes.is_organic == 1)\n ][[\"uid\", \"item_id\"]].drop_duplicates()\n\n graded: dict[int, dict[int, int]] = {}\n for raw_uid, raw_item in strong.itertuples(index=False, name=None):\n uid, item = int(raw_uid), int(raw_item)\n if eligibility.is_eligible(uid, item):\n graded.setdefault(uid, {})[item] = 1\n for raw_uid, raw_item in organic_likes.itertuples(index=False, name=None):\n uid, item = int(raw_uid), int(raw_item)\n if eligibility.is_eligible(uid, item):\n graded.setdefault(uid, {})[item] = 3\n return graded\n\n\ndef load_submission(\n path: Path, eligibility: CandidateSet, targets: set[int]\n) -> dict[int, list[int]]:\n \"\"\"Load and validate a public validation ranking.\n\n Files ending in ``.parquet`` are read as parquet; every other suffix is\n read as CSV. The required columns must be integer-valued, cover exactly the\n target-user set, contain 100 rows and ranks 1 through 100 per user, avoid\n duplicate user-item pairs, and contain only cutoff-eligible items.\n\n Args:\n path: Ranking file to read.\n eligibility: Validation-cutoff candidate membership service.\n targets: Exact set of users the ranking must cover.\n\n Returns:\n A ``uid -> ranked item IDs`` mapping ordered by ascending rank.\n\n Raises:\n ValueError: If the ranking violates the required schema or invariants.\n \"\"\"\n frame = validate_ranking(read_ranking(path), targets)\n if not all(\n eligibility.is_eligible(uid, item)\n for uid, item in frame[[\"uid\", \"item_id\"]].itertuples(index=False, name=None)\n ):\n raise RankingValidationError(\n \"submission contains an item rejected by eligibility service\"\n )\n return ranked_items(frame)\n\n\ndef score(\n ranked: dict[int, list[int]],\n graded: dict[int, dict[int, int]],\n) -> dict[str, float | int]:\n \"\"\"Aggregate per-user NDCG@10 over users with public relevance labels.\n\n Args:\n ranked: Ranked item IDs for every target user.\n graded: Eligible validation relevance keyed by user and item.\n\n Returns:\n The six-decimal ``quality_ndcg@10`` value and number of scored users.\n \"\"\"\n return score_rankings(ranked, graded)\n\n\ndef main() -> None:\n \"\"\"Run the public evaluator CLI and print its JSON score report.\"\"\"\n targets_frame = pd.read_parquet(DATA / \"target_users.parquet\")\n targets = set(targets_frame.uid.astype(int))\n eligibility = CandidateSet(DATA / \"validation_eligible_candidates.npz\")\n graded = validation_truth(eligibility, targets)\n\n if len(sys.argv) != 2:\n raise SystemExit(\n \"usage: evaluate_public.py <validation-ranking.parquet|csv>\"\n )\n path = Path(sys.argv[1])\n ranked = load_submission(path, eligibility, targets)\n print(json.dumps({\"model\": str(path), \"scores\": score(ranked, graded)}, indent=2))\n\n\nif __name__ == \"__main__\":\n main()\n",
"stderr": "",
"aggregated_output": "Traceback (most recent call last):\n File \"/task/current_model/train_current_model.py\", line 28, in <module>\n import numpy as np\nModuleNotFoundError: No module named 'numpy'\n/usr/local/bin/python\n/usr/local/bin/python\n/usr/local/bin/python-config\n/usr/local/bin/python3\n/usr/local/bin/python3-config\n/usr/local/bin/python3.12\n/usr/local/bin/python3.12-config\ndef build_item_correction(\n prepared: SequenceData,\n config: Config,\n target_starts: np.ndarray,\n target_counts: np.ndarray,\n) -> torch.Tensor:\n \"\"\"Estimate in-batch propensities from the selected training targets.\"\"\"\n counts = np.zeros(len(prepared.item_ids), dtype=np.float64)\n for user_row, count in enumerate(target_counts):\n if not count:\n continue\n offset = int(prepared.offsets[user_row])\n target_start = int(target_starts[user_row])\n counts += np.bincount(\n prepared.sequence_items[\n offset\n + target_start\n + 1 : offset\n + target_start\n + int(count)\n + 1\n ].astype(np.int64),\n minlength=len(prepared.item_ids),\n )\n total = float(counts.sum())\n probabilities = (counts + 1.0) / (total + len(counts))\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 hidden = torch.zeros(\n 1, len(users), config.embedding_dim, device=device\n )\n max_targets = int(group_counts.max())\n for start in range(0, max_targets, config.bptt_steps):\n steps = min(config.bptt_steps, max_targets - start)\n block = build_dense_block(\n prepared,\n target_weights,\n users,\n window_starts[users],\n group_counts,\n np.zeros(len(users), dtype=np.int64),\n start,\n steps,\n config,\n )\n sequence_outputs, _ = event_sequence(\n model,\n torch.from_numpy(block[0].astype(np.int64)).to(device),\n torch.from_numpy(block[1].astype(np.float32)).to(device),\n hidden,\n )\n hidden = torch.stack(\n [\n sequence_outputs[row, count - 1]\n if count\n else hidden[0, row]\n for row, count in enumerate(block[7])\n ]\n ).unsqueeze(0).detach()\n flat_scored = block[5].reshape(-1)\n if not np.any(flat_scored):\n continue\n mask = torch.from_numpy(flat_scored).to(device)\n sequence_vectors = sequence_outputs.reshape(\n -1, config.embedding_dim\n )[mask]\n flat_states = block[3].reshape(-1)[flat_scored]\n positive_items = torch.from_numpy(\n block[4].reshape(-1)[flat_scored].astype(np.int64)\n ).to(device)\n known_history = torch.from_numpy(\n block[6].reshape(-1, config.bptt_steps)[flat_scored].astype(\n np.int64\n )\n ).to(device)\n liked_items = torch.from_numpy(\n prepared.liked_states[flat_states].astype(np.int64)\n ).to(device)\n flat_user_rows = np.broadcast_to(\n users[:, None], block[5].shape\n ).reshape(-1)[flat_scored]\n negatives, negative_weights = draw_training_negatives(\n positive_items,\n known_history,\n liked_items,\n flat_user_rows,\n known_positive_packed,\n prepared,\n config,\n item_correction,\n generator,\n device,\n )\n optimizer.zero_grad(set_to_none=True)\n statistics, sequence_gradient = score_target_microbatches(\n model,\n prepared,\n config,\n sequence_vectors,\n flat_states,\n block[2].reshape(-1, 4)[flat_scored].astype(np.float32),\n positive_items,\n negatives,\n negative_weights,\n block[8].reshape(-1)[flat_scored],\n device,\n backward=True,\n )\n if sequence_gradient is None:\n raise RuntimeError(\"training block omitted its sequence gradient\")\n sequence_vectors.backward(sequence_gradient)\n nn.utils.clip_grad_norm_(model.parameters(), config.gradient_clip)\n optimizer.step()\n\n count = len(positive_items)\n total_loss += statistics[\"loss_sum\"]\n total_targets += count\n total_weight += statistics[\"weight_sum\"]\n total_positive += statistics[\"positive_sum\"]\n total_negative += statistics[\"negative_sum\"]\n total_pair_correct += statistics[\"pair_correct\"]\n batches += 1\n if batches % 25 == 0:\n release_accelerator_cache(device)\n if batches % 250 == 0:\n batch_bce = statistics[\"loss_sum\"] / max(\n statistics[\"weight_sum\"], 1.0e-12\n )\n running_bce = total_loss / max(total_weight, 1.0e-12)\n log(\n f\"epoch={epoch}/{config.epochs} batches={batches:,} \"\n f\"targets={total_targets:,} \"\n f\"batch_bce={batch_bce:.6f} \"\n f\"running_bce={running_bce:.6f} \"\n f\"elapsed={time.monotonic() - started:.1f}s\"\n )\n\n return {\n \"epoch\": float(epoch),\n \"loss\": total_loss / max(total_weight, 1.0e-12),\n \"positive_logit\": total_positive / max(total_targets, 1),\n \"negative_logit\": total_negative / max(total_targets * negative_count, 1),\n \"pair_accuracy\": total_pair_correct\n / max(total_targets * negative_count, 1),\n \"targets\": float(total_targets),\n \"target_weight_sum\": total_weight,\n \"batches\": float(batches),\n \"seconds\": time.monotonic() - started,\n }\n\n\[email protected]_grad()\ndef evaluate_validation_bce(\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#!/usr/bin/env python3\n\"\"\"Score a validation-stage ranking against public discovery labels.\n\n``build_task.py`` publishes this module with the solver-visible task files. It\nloads target users, validation eligibility, and validation-window events from\nthe adjacent ``data`` directory, then evaluates one ranking supplied on the\ncommand line. Stage-independent mechanics come from the adjacent public\n``ranking_evaluation.py`` module. The builder installs the same checked-in\nmodule in the private judge, so public and private scoring mechanics cannot\ndrift. A relevant candidate receives grade 3 for an organic like or grade 1 for\nan organic listen with at least 80 percent completion; a like takes precedence\nwhen both events exist.\n\nBefore scoring, the ranking must contain exactly 100 distinct eligible items\nfor every target user, with integer-valued ``uid``, ``item_id``, and ``rank``\ncolumns and ranks 1 through 100 exactly once per user. Extra columns are\nignored. Quality is graded NDCG@10, averaged only over users who have at least\none eligible relevant item in the validation window.\n\nThe evaluator prints a JSON object containing the model path and scores. It\ndoes not apply the task's pass threshold; its purpose is model development and\nselection. The private judge applies the threshold to the final submission.\n\nUsage::\n\n python evaluate_public.py <validation-ranking.parquet|csv>\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport sys\nfrom pathlib import Path\n\nimport pandas as pd\n\nfrom ranking_evaluation import (\n CandidateSet,\n RankingValidationError,\n ranked_items,\n read_ranking,\n score_rankings,\n validate_ranking,\n)\n\n\nHERE = Path(__file__).resolve().parent\nDATA = HERE / \"data\"\nLISTEN_STRONG = 80\n\n\ndef validation_truth(\n eligibility: CandidateSet, targets: set[int]\n) -> dict[int, dict[int, int]]:\n \"\"\"Build cutoff-correct graded relevance from public validation events.\n\n Only target-user events for eligible candidates are retained. Organic\n listens completed to at least 80 percent receive grade 1, and organic likes\n receive grade 3. Repeated user-item events are collapsed, and processing\n likes second ensures that grade 3 wins over grade 1.\n\n Args:\n eligibility: Validation-cutoff candidate membership service.\n targets: Complete set of target user IDs.\n\n Returns:\n A nested ``uid -> item_id -> grade`` mapping. Users without an eligible\n relevant event are omitted.\n \"\"\"\n listens = pd.read_parquet(DATA / \"val_listens.parquet\")\n likes = pd.read_parquet(DATA / \"val_likes.parquet\")\n strong = listens[\n listens.uid.isin(targets)\n & (listens.is_organic == 1)\n & (listens.played_ratio_pct >= LISTEN_STRONG)\n ][[\"uid\", \"item_id\"]].drop_duplicates()\n organic_likes = likes[\n likes.uid.isin(targets) & (likes.is_organic == 1)\n ][[\"uid\", \"item_id\"]].drop_duplicates()\n\n graded: dict[int, dict[int, int]] = {}\n for raw_uid, raw_item in strong.itertuples(index=False, name=None):\n uid, item = int(raw_uid), int(raw_item)\n if eligibility.is_eligible(uid, item):\n graded.setdefault(uid, {})[item] = 1\n for raw_uid, raw_item in organic_likes.itertuples(index=False, name=None):\n uid, item = int(raw_uid), int(raw_item)\n if eligibility.is_eligible(uid, item):\n graded.setdefault(uid, {})[item] = 3\n return graded\n\n\ndef load_submission(\n path: Path, eligibility: CandidateSet, targets: set[int]\n) -> dict[int, list[int]]:\n \"\"\"Load and validate a public validation ranking.\n\n Files ending in ``.parquet`` are read as parquet; every other suffix is\n read as CSV. The required columns must be integer-valued, cover exactly the\n target-user set, contain 100 rows and ranks 1 through 100 per user, avoid\n duplicate user-item pairs, and contain only cutoff-eligible items.\n\n Args:\n path: Ranking file to read.\n eligibility: Validation-cutoff candidate membership service.\n targets: Exact set of users the ranking must cover.\n\n Returns:\n A ``uid -> ranked item IDs`` mapping ordered by ascending rank.\n\n Raises:\n ValueError: If the ranking violates the required schema or invariants.\n \"\"\"\n frame = validate_ranking(read_ranking(path), targets)\n if not all(\n eligibility.is_eligible(uid, item)\n for uid, item in frame[[\"uid\", \"item_id\"]].itertuples(index=False, name=None)\n ):\n raise RankingValidationError(\n \"submission contains an item rejected by eligibility service\"\n )\n return ranked_items(frame)\n\n\ndef score(\n ranked: dict[int, list[int]],\n graded: dict[int, dict[int, int]],\n) -> dict[str, float | int]:\n \"\"\"Aggregate per-user NDCG@10 over users with public relevance labels.\n\n Args:\n ranked: Ranked item IDs for every target user.\n graded: Eligible validation relevance keyed by user and item.\n\n Returns:\n The six-decimal ``quality_ndcg@10`` value and number of scored users.\n \"\"\"\n return score_rankings(ranked, graded)\n\n\ndef main() -> None:\n \"\"\"Run the public evaluator CLI and print its JSON score report.\"\"\"\n targets_frame = pd.read_parquet(DATA / \"target_users.parquet\")\n targets = set(targets_frame.uid.astype(int))\n eligibility = CandidateSet(DATA / \"validation_eligible_candidates.npz\")\n graded = validation_truth(eligibility, targets)\n\n if len(sys.argv) != 2:\n raise SystemExit(\n \"usage: evaluate_public.py <validation-ranking.parquet|csv>\"\n )\n path = Path(sys.argv[1])\n ranked = load_submission(path, eligibility, targets)\n print(json.dumps({\"model\": str(path), \"scores\": score(ranked, graded)}, indent=2))\n\n\nif __name__ == \"__main__\":\n main()\n",
"exit_code": 0,
"duration": {
"secs": 0,
"nanos": 4460
},
"formatted_output": "Traceback (most recent call last):\n File \"/task/current_model/train_current_model.py\", line 28, in <module>\n import numpy as np\nModuleNotFoundError: No module named 'numpy'\n/usr/local/bin/python\n/usr/local/bin/python\n/usr/local/bin/python-config\n/usr/local/bin/python3\n/usr/local/bin/python3-config\n/usr/local/bin/python3.12\n/usr/local/bin/python3.12-config\ndef build_item_correction(\n prepared: SequenceData,\n config: Config,\n target_starts: np.ndarray,\n target_counts: np.ndarray,\n) -> torch.Tensor:\n \"\"\"Estimate in-batch propensities from the selected training targets.\"\"\"\n counts = np.zeros(len(prepared.item_ids), dtype=np.float64)\n for user_row, count in enumerate(target_counts):\n if not count:\n continue\n offset = int(prepared.offsets[user_row])\n target_start = int(target_starts[user_row])\n counts += np.bincount(\n prepared.sequence_items[\n offset\n + target_start\n + 1 : offset\n + target_start\n + int(count)\n + 1\n ].astype(np.int64),\n minlength=len(prepared.item_ids),\n )\n total = float(counts.sum())\n probabilities = (counts + 1.0) / (total + len(counts))\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 hidden = torch.zeros(\n 1, len(users), config.embedding_dim, device=device\n )\n max_targets = int(group_counts.max())\n for start in range(0, max_targets, config.bptt_steps):\n steps = min(config.bptt_steps, max_targets - start)\n block = build_dense_block(\n prepared,\n target_weights,\n users,\n window_starts[users],\n group_counts,\n np.zeros(len(users), dtype=np.int64),\n start,\n steps,\n config,\n )\n sequence_outputs, _ = event_sequence(\n model,\n torch.from_numpy(block[0].astype(np.int64)).to(device),\n torch.from_numpy(block[1].astype(np.float32)).to(device),\n hidden,\n )\n hidden = torch.stack(\n [\n sequence_outputs[row, count - 1]\n if count\n else hidden[0, row]\n for row, count in enumerate(block[7])\n ]\n ).unsqueeze(0).detach()\n flat_scored = block[5].reshape(-1)\n if not np.any(flat_scored):\n continue\n mask = torch.from_numpy(flat_scored).to(device)\n sequence_vectors = sequence_outputs.reshape(\n -1, config.embedding_dim\n )[mask]\n flat_states = block[3].reshape(-1)[flat_scored]\n positive_items = torch.from_numpy(\n block[4].reshape(-1)[flat_scored].astype(np.int64)\n ).to(device)\n known_history = torch.from_numpy(\n block[6].reshape(-1, config.bptt_steps)[flat_scored].astype(\n np.int64\n )\n ).to(device)\n liked_items = torch.from_numpy(\n prepared.liked_states[flat_states].astype(np.int64)\n ).to(device)\n flat_user_rows = np.broadcast_to(\n users[:, None], block[5].shape\n ).reshape(-1)[flat_scored]\n negatives, negative_weights = draw_training_negatives(\n positive_items,\n known_history,\n liked_items,\n flat_user_rows,\n known_positive_packed,\n prepared,\n config,\n item_correction,\n generator,\n device,\n )\n optimizer.zero_grad(set_to_none=True)\n statistics, sequence_gradient = score_target_microbatches(\n model,\n prepared,\n config,\n sequence_vectors,\n flat_states,\n block[2].reshape(-1, 4)[flat_scored].astype(np.float32),\n positive_items,\n negatives,\n negative_weights,\n block[8].reshape(-1)[flat_scored],\n device,\n backward=True,\n )\n if sequence_gradient is None:\n raise RuntimeError(\"training block omitted its sequence gradient\")\n sequence_vectors.backward(sequence_gradient)\n nn.utils.clip_grad_norm_(model.parameters(), config.gradient_clip)\n optimizer.step()\n\n count = len(positive_items)\n total_loss += statistics[\"loss_sum\"]\n total_targets += count\n total_weight += statistics[\"weight_sum\"]\n total_positive += statistics[\"positive_sum\"]\n total_negative += statistics[\"negative_sum\"]\n total_pair_correct += statistics[\"pair_correct\"]\n batches += 1\n if batches % 25 == 0:\n release_accelerator_cache(device)\n if batches % 250 == 0:\n batch_bce = statistics[\"loss_sum\"] / max(\n statistics[\"weight_sum\"], 1.0e-12\n )\n running_bce = total_loss / max(total_weight, 1.0e-12)\n log(\n f\"epoch={epoch}/{config.epochs} batches={batches:,} \"\n f\"targets={total_targets:,} \"\n f\"batch_bce={batch_bce:.6f} \"\n f\"running_bce={running_bce:.6f} \"\n f\"elapsed={time.monotonic() - started:.1f}s\"\n )\n\n return {\n \"epoch\": float(epoch),\n \"loss\": total_loss / max(total_weight, 1.0e-12),\n \"positive_logit\": total_positive / max(total_targets, 1),\n \"negative_logit\": total_negative / max(total_targets * negative_count, 1),\n \"pair_accuracy\": total_pair_correct\n / max(total_targets * negative_count, 1),\n \"targets\": float(total_targets),\n \"target_weight_sum\": total_weight,\n \"batches\": float(batches),\n \"seconds\": time.monotonic() - started,\n }\n\n\[email protected]_grad()\ndef evaluate_validation_bce(\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#!/usr/bin/env python3\n\"\"\"Score a validation-stage ranking against public discovery labels.\n\n``build_task.py`` publishes this module with the solver-visible task files. It\nloads target users, validation eligibility, and validation-window events from\nthe adjacent ``data`` directory, then evaluates one ranking supplied on the\ncommand line. Stage-independent mechanics come from the adjacent public\n``ranking_evaluation.py`` module. The builder installs the same checked-in\nmodule in the private judge, so public and private scoring mechanics cannot\ndrift. A relevant candidate receives grade 3 for an organic like or grade 1 for\nan organic listen with at least 80 percent completion; a like takes precedence\nwhen both events exist.\n\nBefore scoring, the ranking must contain exactly 100 distinct eligible items\nfor every target user, with integer-valued ``uid``, ``item_id``, and ``rank``\ncolumns and ranks 1 through 100 exactly once per user. Extra columns are\nignored. Quality is graded NDCG@10, averaged only over users who have at least\none eligible relevant item in the validation window.\n\nThe evaluator prints a JSON object containing the model path and scores. It\ndoes not apply the task's pass threshold; its purpose is model development and\nselection. The private judge applies the threshold to the final submission.\n\nUsage::\n\n python evaluate_public.py <validation-ranking.parquet|csv>\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport sys\nfrom pathlib import Path\n\nimport pandas as pd\n\nfrom ranking_evaluation import (\n CandidateSet,\n RankingValidationError,\n ranked_items,\n read_ranking,\n score_rankings,\n validate_ranking,\n)\n\n\nHERE = Path(__file__).resolve().parent\nDATA = HERE / \"data\"\nLISTEN_STRONG = 80\n\n\ndef validation_truth(\n eligibility: CandidateSet, targets: set[int]\n) -> dict[int, dict[int, int]]:\n \"\"\"Build cutoff-correct graded relevance from public validation events.\n\n Only target-user events for eligible candidates are retained. Organic\n listens completed to at least 80 percent receive grade 1, and organic likes\n receive grade 3. Repeated user-item events are collapsed, and processing\n likes second ensures that grade 3 wins over grade 1.\n\n Args:\n eligibility: Validation-cutoff candidate membership service.\n targets: Complete set of target user IDs.\n\n Returns:\n A nested ``uid -> item_id -> grade`` mapping. Users without an eligible\n relevant event are omitted.\n \"\"\"\n listens = pd.read_parquet(DATA / \"val_listens.parquet\")\n likes = pd.read_parquet(DATA / \"val_likes.parquet\")\n strong = listens[\n listens.uid.isin(targets)\n & (listens.is_organic == 1)\n & (listens.played_ratio_pct >= LISTEN_STRONG)\n ][[\"uid\", \"item_id\"]].drop_duplicates()\n organic_likes = likes[\n likes.uid.isin(targets) & (likes.is_organic == 1)\n ][[\"uid\", \"item_id\"]].drop_duplicates()\n\n graded: dict[int, dict[int, int]] = {}\n for raw_uid, raw_item in strong.itertuples(index=False, name=None):\n uid, item = int(raw_uid), int(raw_item)\n if eligibility.is_eligible(uid, item):\n graded.setdefault(uid, {})[item] = 1\n for raw_uid, raw_item in organic_likes.itertuples(index=False, name=None):\n uid, item = int(raw_uid), int(raw_item)\n if eligibility.is_eligible(uid, item):\n graded.setdefault(uid, {})[item] = 3\n return graded\n\n\ndef load_submission(\n path: Path, eligibility: CandidateSet, targets: set[int]\n) -> dict[int, list[int]]:\n \"\"\"Load and validate a public validation ranking.\n\n Files ending in ``.parquet`` are read as parquet; every other suffix is\n read as CSV. The required columns must be integer-valued, cover exactly the\n target-user set, contain 100 rows and ranks 1 through 100 per user, avoid\n duplicate user-item pairs, and contain only cutoff-eligible items.\n\n Args:\n path: Ranking file to read.\n eligibility: Validation-cutoff candidate membership service.\n targets: Exact set of users the ranking must cover.\n\n Returns:\n A ``uid -> ranked item IDs`` mapping ordered by ascending rank.\n\n Raises:\n ValueError: If the ranking violates the required schema or invariants.\n \"\"\"\n frame = validate_ranking(read_ranking(path), targets)\n if not all(\n eligibility.is_eligible(uid, item)\n for uid, item in frame[[\"uid\", \"item_id\"]].itertuples(index=False, name=None)\n ):\n raise RankingValidationError(\n \"submission contains an item rejected by eligibility service\"\n )\n return ranked_items(frame)\n\n\ndef score(\n ranked: dict[int, list[int]],\n graded: dict[int, dict[int, int]],\n) -> dict[str, float | int]:\n \"\"\"Aggregate per-user NDCG@10 over users with public relevance labels.\n\n Args:\n ranked: Ranked item IDs for every target user.\n graded: Eligible validation relevance keyed by user and item.\n\n Returns:\n The six-decimal ``quality_ndcg@10`` value and number of scored users.\n \"\"\"\n return score_rankings(ranked, graded)\n\n\ndef main() -> None:\n \"\"\"Run the public evaluator CLI and print its JSON score report.\"\"\"\n targets_frame = pd.read_parquet(DATA / \"target_users.parquet\")\n targets = set(targets_frame.uid.astype(int))\n eligibility = CandidateSet(DATA / \"validation_eligible_candidates.npz\")\n graded = validation_truth(eligibility, targets)\n\n if len(sys.argv) != 2:\n raise SystemExit(\n \"usage: evaluate_public.py <validation-ranking.parquet|csv>\"\n )\n path = Path(sys.argv[1])\n ranked = load_submission(path, eligibility, targets)\n print(json.dumps({\"model\": str(path), \"scores\": score(ranked, graded)}, indent=2))\n\n\nif __name__ == \"__main__\":\n main()\n"
}
}