5050
5151import torch
5252
53- from torchwright .graph import Node , Concatenate , Attn
53+ from torchwright .graph import Node , Concatenate , Attn , LiteralValue
5454from torchwright .graph .asserts import (
5555 assert_in_range ,
5656 assert_matches_value_type ,
@@ -118,8 +118,9 @@ def _wrap_hard_selection_output(
118118_QUERY_GAIN = 8.0
119119
120120# Direct (not gained) logit bonus for valid positions in the simple
121- # ``_where`` variants (``attend_argmin_where``, ``attend_argmax_where``,
122- # ``attend_mean_where``). Routed through a dedicated ``d_qk`` column
121+ # scalar ``_where`` variants (``attend_argmin_where``,
122+ # ``attend_argmax_where``, ``attend_mean_where``) and the dot-product
123+ # ``_dot_where`` variants. Routed through a dedicated ``d_qk`` column
123124# with ``Q = 1.0`` and ``K = ± _VALIDITY_DIRECT``, so the contribution
124125# to the logit is literally ``± _VALIDITY_DIRECT`` — not multiplied by
125126# ``_QUERY_GAIN``. Must exceed the one-sided score swing
@@ -143,6 +144,14 @@ def _wrap_hard_selection_output(
143144# ``attend_argmin_valid_unmasked``).
144145_MAX_SCORE_ABS = 120.0
145146
147+ # Maximum absolute dot-product contribution supported by
148+ # ``attend_argmax_dot_where`` / ``attend_argmin_dot_where`` *after*
149+ # multiplying by ``match_gain``. Validity contributes
150+ # ``± _VALIDITY_DIRECT`` directly to the logit, so keeping the match
151+ # term inside ±960 leaves the same 40-logit margin as the scalar
152+ # ``_where`` variants in the worst valid-vs-invalid case.
153+ _MAX_DOT_LOGIT_ABS = 960.0
154+
146155# Penalty (in *logit* space, not key space) applied by
147156# ``attend_argmin_unmasked`` to masked positions. Must exceed
148157# ``_QUERY_GAIN * _MAX_SCORE_UNMASKED_ABS`` so a masked position with
@@ -1004,6 +1013,177 @@ def attend_argmax_dot(
10041013 )
10051014
10061015
1016+ def _build_dot_where_attn (
1017+ query_vector : Node ,
1018+ key_vector : Node ,
1019+ validity : Node ,
1020+ value : Node ,
1021+ * ,
1022+ score_sign : float ,
1023+ match_gain : float ,
1024+ ) -> Attn :
1025+ """Shared construction for dot-product selection with validity.
1026+
1027+ ``score_sign`` is ``+1`` for argmax and ``-1`` for argmin.
1028+ """
1029+ assert len (query_vector ) == len (key_vector ), (
1030+ "query_vector and key_vector must have the same width "
1031+ f"(got { len (query_vector )} and { len (key_vector )} )"
1032+ )
1033+ assert len (validity ) == 1 , "attend_*_dot_where expects 1D boolean validity"
1034+ assert match_gain > 0 , "attend_*_dot_where expects positive match_gain"
1035+
1036+ W = len (query_vector )
1037+ d_v = len (value )
1038+ # d_qk layout:
1039+ # cols 0..W-1: dot-product score
1040+ # col W: direct validity bonus
1041+ d_qk = W + 1
1042+
1043+ # The validity term needs a query-side constant. A LiteralValue keeps
1044+ # this op independent of PosEncoding while preserving the same direct
1045+ # validity-logit pattern used by the scalar _where variants.
1046+ query_one = LiteralValue (torch .tensor ([1.0 ]), name = "dot_where_query_one" )
1047+ query_in = Concatenate ([query_vector , query_one ])
1048+ key_in = Concatenate ([key_vector , validity ])
1049+
1050+ query_matrix = torch .zeros ((W + 1 , d_qk ))
1051+ for c in range (W ):
1052+ query_matrix [c , c ] = score_sign * match_gain
1053+ query_matrix [W , W ] = 1.0
1054+
1055+ key_matrix = torch .zeros ((W + 1 , d_qk ))
1056+ for c in range (W ):
1057+ key_matrix [c , c ] = 1.0
1058+ key_matrix [W , W ] = _VALIDITY_DIRECT
1059+
1060+ value_matrix = torch .eye (d_v )
1061+ output_matrix = torch .eye (d_v )
1062+
1063+ return Attn (
1064+ query_in = query_in ,
1065+ key_in = key_in ,
1066+ value_in = value ,
1067+ query_matrix = query_matrix ,
1068+ key_matrix = key_matrix ,
1069+ value_matrix = value_matrix ,
1070+ output_matrix = output_matrix ,
1071+ )
1072+
1073+
1074+ def attend_argmax_dot_where (
1075+ query_vector : Node ,
1076+ key_vector : Node ,
1077+ validity : Node ,
1078+ value : Node ,
1079+ match_gain : float = 200.0 ,
1080+ assert_hardness_gt : Optional [float ] = None ,
1081+ ) -> Node :
1082+ """Argmax of ``query_vector · key_vector`` over valid key positions.
1083+
1084+ At each query position, returns ``value`` at the causal-window
1085+ position where ``validity`` is +1 and the dot product with the query
1086+ vector is largest. ``validity`` follows the usual torchwright
1087+ boolean convention: +1.0 means "valid", -1.0 means "invalid".
1088+
1089+ The logit at key position ``i`` seen from query position ``j`` is
1090+
1091+ match_gain · (query_vector[j] · key_vector[i])
1092+ + _VALIDITY_DIRECT · validity[i]
1093+
1094+ Invalid rows cannot win as long as every caller-provided dot score
1095+ satisfies ``abs(match_gain * dot_score) <= _MAX_DOT_LOGIT_ABS``.
1096+ With the default ``match_gain=200``, this means
1097+ ``abs(dot_score) <= 4.8``. If your dot products are larger, scale
1098+ the vectors or lower ``match_gain`` so the validity term has room to
1099+ dominate; tied valid scores produce a soft average.
1100+
1101+ **When no position is valid.** The softmax still returns a weighted
1102+ average over invalid positions. Callers must ensure each consumed
1103+ query has at least one valid causal-window key, or gate the result
1104+ downstream.
1105+
1106+ Compile cost: one attention head (auto-split across multiple
1107+ physical heads by the compiler when ``d_v > d_head``).
1108+ ``d_qk = len(query_vector) + 1``, ``d_v = len(value)``.
1109+
1110+ Args:
1111+ query_vector: Width-``W`` node at each query position.
1112+ key_vector: Width-``W`` node at each key position.
1113+ validity: 1D boolean node (+1 valid, -1 invalid).
1114+ value: Node to read at the winning position.
1115+ match_gain: Positive coefficient applied to the dot-product term.
1116+ assert_hardness_gt: If set, checks the winning softmax weight.
1117+
1118+ Returns:
1119+ Attn node of width ``len(value)``.
1120+
1121+ See also:
1122+ :func:`attend_argmin_dot_where` — minimum-dot dual.
1123+ :func:`attend_argmax_dot` — original unmasked dot-product variant.
1124+ """
1125+ attn = _build_dot_where_attn (
1126+ query_vector ,
1127+ key_vector ,
1128+ validity ,
1129+ value ,
1130+ score_sign = + 1.0 ,
1131+ match_gain = match_gain ,
1132+ )
1133+ return _wrap_hard_selection_output (
1134+ attn , value , assert_hardness_gt = assert_hardness_gt
1135+ )
1136+
1137+
1138+ def attend_argmin_dot_where (
1139+ query_vector : Node ,
1140+ key_vector : Node ,
1141+ validity : Node ,
1142+ value : Node ,
1143+ match_gain : float = 200.0 ,
1144+ assert_hardness_gt : Optional [float ] = None ,
1145+ ) -> Node :
1146+ """Argmin of ``query_vector · key_vector`` over valid key positions.
1147+
1148+ Sign-flipped twin of :func:`attend_argmax_dot_where`. This is the
1149+ variant to use when zeroed invalid keys are unsafe: in an argmin, an
1150+ invalid zero key can otherwise beat valid keys with positive dot
1151+ scores.
1152+
1153+ The logit at key position ``i`` seen from query position ``j`` is
1154+
1155+ -match_gain · (query_vector[j] · key_vector[i])
1156+ + _VALIDITY_DIRECT · validity[i]
1157+
1158+ The same supported range applies:
1159+ ``abs(match_gain * dot_score) <= _MAX_DOT_LOGIT_ABS`` for all key
1160+ rows in the consumed causal window. At the default
1161+ ``match_gain=200``, that is ``abs(dot_score) <= 4.8``.
1162+
1163+ Args:
1164+ query_vector: Width-``W`` node at each query position.
1165+ key_vector: Width-``W`` node at each key position.
1166+ validity: 1D boolean node (+1 valid, -1 invalid).
1167+ value: Node to read at the winning position.
1168+ match_gain: Positive coefficient applied to the dot-product term.
1169+ assert_hardness_gt: If set, checks the winning softmax weight.
1170+
1171+ Returns:
1172+ Attn node of width ``len(value)``.
1173+ """
1174+ attn = _build_dot_where_attn (
1175+ query_vector ,
1176+ key_vector ,
1177+ validity ,
1178+ value ,
1179+ score_sign = - 1.0 ,
1180+ match_gain = match_gain ,
1181+ )
1182+ return _wrap_hard_selection_output (
1183+ attn , value , assert_hardness_gt = assert_hardness_gt
1184+ )
1185+
1186+
10071187def attend_most_recent_matching (
10081188 pos_encoding : PosEncoding ,
10091189 query_vector : Node ,
0 commit comments