polgrad 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
polgrad/__init__.py ADDED
@@ -0,0 +1,93 @@
1
+ """Reference semantics, conformance testing, and pathology diagnostics for LLM
2
+ policy-gradient post-training.
3
+
4
+ The core namespace exports the loss algebra, KL estimators, advantage estimators, and the
5
+ algorithm registry. ``polgrad.diagnostics``, ``polgrad.verify``, and
6
+ ``polgrad.conformance`` are subpackages with their own exports. Conventions (shapes,
7
+ masking, signs, dtypes) are specified in ``docs/conventions.md`` and enforced by tests.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from polgrad.advantages import (
13
+ GAEConfig,
14
+ GroupNormConfig,
15
+ ReinforcePPConfig,
16
+ broadcast_to_tokens,
17
+ gae,
18
+ grpo_advantages,
19
+ reinforce_pp_advantages,
20
+ rloo_advantages,
21
+ whiten,
22
+ )
23
+ from polgrad.aggregate import (
24
+ Aggregation,
25
+ aggregate,
26
+ effective_token_weights,
27
+ microbatch_token_weights,
28
+ )
29
+ from polgrad.kl import (
30
+ KLEstimator,
31
+ KLLossConfig,
32
+ kl_estimate,
33
+ kl_in_reward,
34
+ kl_loss,
35
+ reverse_kl_grad_surrogate,
36
+ )
37
+ from polgrad.losses import (
38
+ ClipConfig,
39
+ ISCorrectionConfig,
40
+ PolicyLossConfig,
41
+ PolicyLossResult,
42
+ RatioKind,
43
+ SurrogateKind,
44
+ ValueLossResult,
45
+ policy_loss,
46
+ value_loss,
47
+ )
48
+ from polgrad.registry import (
49
+ ALGORITHMS,
50
+ AlgorithmSpec,
51
+ Citation,
52
+ )
53
+ from polgrad.registry import describe as describe_algorithm
54
+ from polgrad.registry import get as get_algorithm
55
+
56
+ __version__ = "0.1.0"
57
+
58
+ __all__ = [
59
+ "ALGORITHMS",
60
+ "Aggregation",
61
+ "AlgorithmSpec",
62
+ "Citation",
63
+ "ClipConfig",
64
+ "GAEConfig",
65
+ "GroupNormConfig",
66
+ "ISCorrectionConfig",
67
+ "KLEstimator",
68
+ "KLLossConfig",
69
+ "PolicyLossConfig",
70
+ "PolicyLossResult",
71
+ "RatioKind",
72
+ "ReinforcePPConfig",
73
+ "SurrogateKind",
74
+ "ValueLossResult",
75
+ "__version__",
76
+ "aggregate",
77
+ "broadcast_to_tokens",
78
+ "describe_algorithm",
79
+ "effective_token_weights",
80
+ "gae",
81
+ "get_algorithm",
82
+ "grpo_advantages",
83
+ "kl_estimate",
84
+ "kl_in_reward",
85
+ "kl_loss",
86
+ "microbatch_token_weights",
87
+ "policy_loss",
88
+ "reinforce_pp_advantages",
89
+ "reverse_kl_grad_surrogate",
90
+ "rloo_advantages",
91
+ "value_loss",
92
+ "whiten",
93
+ ]
polgrad/_validation.py ADDED
@@ -0,0 +1,160 @@
1
+ """Input validation and tiny shared numeric helpers used across polgrad.
2
+
3
+ Single source of error-message style. See ``docs/conventions.md`` for the shape and
4
+ masking rules these helpers enforce. Logprob tensors are validated for finiteness only:
5
+ real frameworks emit slightly positive logprob values from numerics, so rejecting
6
+ ``> 0`` would reject real data. The per-site message parameters of
7
+ :func:`broadcast_advantages` exist because callers' error strings are pinned by tests;
8
+ the wording differences are preserved deliberately, not drift.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import torch
14
+ from torch import Tensor
15
+
16
+ __all__ = [
17
+ "broadcast_advantages",
18
+ "check_1d",
19
+ "check_2d",
20
+ "check_finite",
21
+ "check_logprob_streams",
22
+ "check_mask",
23
+ "check_same_shape",
24
+ "std_or_zero",
25
+ ]
26
+
27
+
28
+ def check_1d(name: str, x: Tensor) -> None:
29
+ """Raise ``ValueError`` unless ``x`` is 1-D ``[B]``."""
30
+ if x.dim() != 1:
31
+ raise ValueError(f"{name} must be 1-D [B]; got shape {tuple(x.shape)}")
32
+
33
+
34
+ def check_2d(name: str, x: Tensor) -> None:
35
+ """Raise ``ValueError`` unless ``x`` is 2-D ``[B, T]``."""
36
+ if x.dim() != 2:
37
+ raise ValueError(f"{name} must be 2-D [B, T]; got shape {tuple(x.shape)}")
38
+
39
+
40
+ def check_same_shape(name_a: str, a: Tensor, name_b: str, b: Tensor) -> None:
41
+ """Raise ``ValueError`` unless ``a`` and ``b`` have identical shapes."""
42
+ if a.shape != b.shape:
43
+ raise ValueError(
44
+ f"{name_a} and {name_b} must have identical shapes; "
45
+ f"got {tuple(a.shape)} vs {tuple(b.shape)}"
46
+ )
47
+
48
+
49
+ def check_finite(name: str, x: Tensor) -> None:
50
+ """Raise ``ValueError`` if ``x`` contains NaN or infinite values."""
51
+ if not bool(torch.isfinite(x).all()):
52
+ raise ValueError(f"{name} contains non-finite values")
53
+
54
+
55
+ def check_mask(response_mask: Tensor, *, like: Tensor) -> None:
56
+ """Validate a response mask against a reference tensor.
57
+
58
+ Enforces the masking rules of ``docs/conventions.md``: ``response_mask`` must be a
59
+ 2-D boolean tensor with the same shape as ``like``, and every row must contain at
60
+ least one response token.
61
+
62
+ Raises:
63
+ ValueError: On dtype, dimensionality, or shape mismatch, or if any row of the
64
+ mask has zero response tokens.
65
+ """
66
+ if response_mask.dtype != torch.bool:
67
+ raise ValueError(f"response_mask must have dtype torch.bool; got {response_mask.dtype}")
68
+ check_2d("response_mask", response_mask)
69
+ if response_mask.shape != like.shape:
70
+ raise ValueError(
71
+ f"response_mask shape {tuple(response_mask.shape)} does not match "
72
+ f"input shape {tuple(like.shape)}"
73
+ )
74
+ row_counts = response_mask.sum(dim=1)
75
+ if int(row_counts.min()) == 0:
76
+ empty = torch.nonzero(row_counts == 0).flatten().tolist()
77
+ raise ValueError(f"response_mask has rows with zero response tokens: rows {empty}")
78
+
79
+
80
+ def check_logprob_streams(
81
+ name_a: str,
82
+ a: Tensor,
83
+ name_b: str,
84
+ b: Tensor,
85
+ response_mask: Tensor,
86
+ *,
87
+ check_b_2d: bool = False,
88
+ finite_suffix: str = "",
89
+ ) -> None:
90
+ """Shared shape/mask/finiteness validation for a pair of logprob streams.
91
+
92
+ Finiteness is checked only at response positions: masked padding never affects any
93
+ output, so non-finite junk there must not raise (mask invariance extends to
94
+ validation). ``check_b_2d`` additionally names ``b`` in its own rank error before
95
+ the shape comparison; ``finite_suffix`` (e.g. ``" (response positions)"``) is
96
+ appended to the finiteness labels. Both parameters preserve the callers' historical
97
+ error strings.
98
+ """
99
+ check_2d(name_a, a)
100
+ if check_b_2d:
101
+ check_2d(name_b, b)
102
+ check_same_shape(name_a, a, name_b, b)
103
+ check_mask(response_mask, like=a)
104
+ check_finite(name_a + finite_suffix, a[response_mask])
105
+ check_finite(name_b + finite_suffix, b[response_mask])
106
+
107
+
108
+ def broadcast_advantages(
109
+ advantages: Tensor,
110
+ like: Tensor,
111
+ response_mask: Tensor,
112
+ *,
113
+ like_name: str,
114
+ zero_masked: bool = False,
115
+ finite_label_2d: str = "advantages (response positions)",
116
+ batch_mismatch_template: str = "advantages [B] must have B = {b} rows; got shape {adv_shape}",
117
+ shape_mismatch_template: str | None = None,
118
+ ) -> Tensor:
119
+ """Validate ``[B]`` or ``[B, T]`` advantages and return them as ``[B, T]``.
120
+
121
+ A ``[B]`` input is expanded across its row's tokens; a ``[B, T]`` input must match
122
+ ``like``'s shape and be finite at response positions. With ``zero_masked=True`` the
123
+ result is exactly 0 at masked positions, so padded advantage junk reaches neither
124
+ forward values nor backward formulas (mask invariance); with ``zero_masked=False``
125
+ the raw broadcast view is returned for callers that intersect with the mask
126
+ themselves. The message templates take ``{b}``, ``{adv_shape}``, and ``{like_shape}``
127
+ placeholders; ``shape_mismatch_template=None`` uses :func:`check_same_shape` with
128
+ ``like_name``.
129
+ """
130
+ if advantages.dim() == 1:
131
+ if advantages.shape[0] != like.shape[0]:
132
+ raise ValueError(
133
+ batch_mismatch_template.format(b=like.shape[0], adv_shape=tuple(advantages.shape))
134
+ )
135
+ check_finite("advantages", advantages)
136
+ expanded = advantages.unsqueeze(1).expand_as(like)
137
+ elif advantages.dim() == 2:
138
+ if shape_mismatch_template is None:
139
+ check_same_shape("advantages", advantages, like_name, like)
140
+ elif advantages.shape != like.shape:
141
+ raise ValueError(
142
+ shape_mismatch_template.format(
143
+ like_shape=tuple(like.shape), adv_shape=tuple(advantages.shape)
144
+ )
145
+ )
146
+ check_finite(finite_label_2d, advantages[response_mask])
147
+ expanded = advantages
148
+ else:
149
+ raise ValueError(f"advantages must be [B] or [B, T]; got shape {tuple(advantages.shape)}")
150
+ if not zero_masked:
151
+ return expanded
152
+ zero = torch.zeros((), dtype=expanded.dtype, device=expanded.device)
153
+ return torch.where(response_mask, expanded, zero)
154
+
155
+
156
+ def std_or_zero(values: Tensor) -> float:
157
+ """Bessel-corrected std; a single observation has no spread estimate, reported as 0.0."""
158
+ if values.numel() < 2:
159
+ return 0.0
160
+ return float(values.std())