|
| 1 | +# Copyright 2026, The FlagOS Contributors. |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | +# |
| 15 | +# Generated by KernelGen: https://github.com/flagos-ai/KernelGen |
| 16 | +import logging |
| 17 | + |
| 18 | +import torch |
| 19 | +import triton |
| 20 | +import triton.language as tl |
| 21 | + |
| 22 | +import flag_gems |
| 23 | + |
| 24 | +logger = logging.getLogger(__name__) |
| 25 | + |
| 26 | +# The denominator is guarded against zero-norm inputs with a literal 1e-8 inside |
| 27 | +# each kernel (Triton @jit bodies cannot read module-level Python globals). This |
| 28 | +# matches the PyTorch reference on the tested float16/bfloat16/float32 workloads. |
| 29 | + |
| 30 | + |
| 31 | +@triton.jit |
| 32 | +def _cosine_embedding_loss_elementwise_kernel( |
| 33 | + x1_ptr, |
| 34 | + x2_ptr, |
| 35 | + y_ptr, |
| 36 | + out_ptr, |
| 37 | + n_rows, |
| 38 | + d, |
| 39 | + x1_stride0, |
| 40 | + x2_stride0, |
| 41 | + margin, |
| 42 | + BLOCK_D: tl.constexpr, |
| 43 | +): |
| 44 | + # One program per row: compute cosine similarity along the last dim, then |
| 45 | + # the per-sample loss, and write it to out[row]. |
| 46 | + row = tl.program_id(axis=0) |
| 47 | + if row >= n_rows: |
| 48 | + return |
| 49 | + |
| 50 | + cols = tl.arange(0, BLOCK_D) |
| 51 | + mask = cols < d |
| 52 | + |
| 53 | + x1 = tl.load(x1_ptr + row * x1_stride0 + cols, mask=mask, other=0.0).to(tl.float32) |
| 54 | + x2 = tl.load(x2_ptr + row * x2_stride0 + cols, mask=mask, other=0.0).to(tl.float32) |
| 55 | + |
| 56 | + dot = tl.sum(x1 * x2, axis=0) |
| 57 | + norm1 = tl.sqrt(tl.sum(x1 * x1, axis=0)) |
| 58 | + norm2 = tl.sqrt(tl.sum(x2 * x2, axis=0)) |
| 59 | + denom = norm1 * norm2 |
| 60 | + cos = dot / (denom + 1e-8) |
| 61 | + |
| 62 | + y = tl.load(y_ptr + row).to(tl.float32) |
| 63 | + # target == 1 -> 1 - cos ; target == -1 -> max(0, cos - margin) |
| 64 | + is_pos = y > 0.0 |
| 65 | + loss = tl.where(is_pos, 1.0 - cos, tl.maximum(cos - margin, 0.0)) |
| 66 | + |
| 67 | + tl.store(out_ptr + row, loss) |
| 68 | + |
| 69 | + |
| 70 | +@triton.jit |
| 71 | +def _cosine_embedding_loss_per_row_kernel( |
| 72 | + x1_ptr, |
| 73 | + x2_ptr, |
| 74 | + y_ptr, |
| 75 | + mid_ptr, |
| 76 | + n_rows, |
| 77 | + d, |
| 78 | + x1_stride0, |
| 79 | + x2_stride0, |
| 80 | + margin, |
| 81 | + BLOCK_D: tl.constexpr, |
| 82 | +): |
| 83 | + # One program per sample-row: compute the per-row loss in float32 and write |
| 84 | + # it to mid[row]. A 1D (BLOCK_D,) tile keeps every load well under Triton's |
| 85 | + # per-tile numel limit regardless of the row count. The final reduction over |
| 86 | + # mid is done by _cosine_embedding_loss_final_reduce_kernel. |
| 87 | + row = tl.program_id(axis=0) |
| 88 | + if row >= n_rows: |
| 89 | + return |
| 90 | + cols = tl.arange(0, BLOCK_D) |
| 91 | + col_mask = cols < d |
| 92 | + |
| 93 | + x1 = tl.load(x1_ptr + row * x1_stride0 + cols, mask=col_mask, other=0.0).to( |
| 94 | + tl.float32 |
| 95 | + ) |
| 96 | + x2 = tl.load(x2_ptr + row * x2_stride0 + cols, mask=col_mask, other=0.0).to( |
| 97 | + tl.float32 |
| 98 | + ) |
| 99 | + dot = tl.sum(x1 * x2, axis=0) |
| 100 | + norm1 = tl.sqrt(tl.sum(x1 * x1, axis=0)) |
| 101 | + norm2 = tl.sqrt(tl.sum(x2 * x2, axis=0)) |
| 102 | + cos = dot / (norm1 * norm2 + 1e-8) |
| 103 | + |
| 104 | + y = tl.load(y_ptr + row).to(tl.float32) |
| 105 | + is_pos = y > 0.0 |
| 106 | + loss = tl.where(is_pos, 1.0 - cos, tl.maximum(cos - margin, 0.0)) |
| 107 | + tl.store(mid_ptr + row, loss) |
| 108 | + |
| 109 | + |
| 110 | +@triton.jit |
| 111 | +def _cosine_embedding_loss_final_reduce_kernel( |
| 112 | + mid_ptr, out_ptr, mid_size, inv_scale, BLOCK_MID: tl.constexpr |
| 113 | +): |
| 114 | + # Grid-stride reduction so a large partial-sum buffer does not require a |
| 115 | + # single oversized block. Accumulate in float32. inv_scale folds the mean |
| 116 | + # division into the kernel (inv_scale=1/n_rows for mean, 1.0 for sum) so the |
| 117 | + # host never issues a tensor/scalar div that use_gems would re-dispatch. |
| 118 | + offsets = tl.arange(0, BLOCK_MID) |
| 119 | + acc = 0.0 |
| 120 | + for start in range(0, mid_size, BLOCK_MID): |
| 121 | + idx = start + offsets |
| 122 | + mask = idx < mid_size |
| 123 | + vals = tl.load(mid_ptr + idx, mask=mask, other=0.0).to(tl.float32) |
| 124 | + acc += tl.sum(vals, axis=0) |
| 125 | + # Store into out_ptr's own dtype so the host never issues a `.to()` cast |
| 126 | + # (which use_gems would re-dispatch to a pointwise _to_copy kernel). |
| 127 | + tl.store(out_ptr, (acc * inv_scale).to(out_ptr.dtype.element_ty)) |
| 128 | + |
| 129 | + |
| 130 | +def _normalize_reduction(reduction): |
| 131 | + # Accept both string and enum/int forms: 0=none,1=mean,2=sum |
| 132 | + if isinstance(reduction, str): |
| 133 | + r = reduction.lower() |
| 134 | + if r == "none": |
| 135 | + return 0 |
| 136 | + if r == "mean": |
| 137 | + return 1 |
| 138 | + if r == "sum": |
| 139 | + return 2 |
| 140 | + raise ValueError(f"Invalid reduction: {reduction}") |
| 141 | + if isinstance(reduction, int): |
| 142 | + if reduction in (0, 1, 2): |
| 143 | + return reduction |
| 144 | + raise ValueError(f"Invalid reduction int: {reduction}") |
| 145 | + raise ValueError(f"Unsupported reduction type: {type(reduction)}") |
| 146 | + |
| 147 | + |
| 148 | +def _check_tensors(input1: torch.Tensor, input2: torch.Tensor, target: torch.Tensor): |
| 149 | + if input1.device.type != flag_gems.device or input2.device.type != flag_gems.device: |
| 150 | + raise AssertionError( |
| 151 | + f"cosine_embedding_loss: inputs must be {flag_gems.device} tensors for Triton kernel." |
| 152 | + ) |
| 153 | + if input1.device != input2.device or input1.device != target.device: |
| 154 | + raise AssertionError( |
| 155 | + "cosine_embedding_loss: all inputs must be on the same device." |
| 156 | + ) |
| 157 | + if input1.shape != input2.shape: |
| 158 | + raise AssertionError( |
| 159 | + "cosine_embedding_loss: input1 and input2 must have the same shape." |
| 160 | + ) |
| 161 | + if input1.dim() < 1: |
| 162 | + raise AssertionError( |
| 163 | + "cosine_embedding_loss: inputs must have at least one dimension." |
| 164 | + ) |
| 165 | + # target is (N,) for batched inputs (input.ndim >= 2); for 1-D inputs target is (). |
| 166 | + expected_target_shape = input1.shape[:-1] if input1.dim() >= 2 else () |
| 167 | + if target.shape != expected_target_shape: |
| 168 | + raise AssertionError( |
| 169 | + f"cosine_embedding_loss: target must have shape {expected_target_shape}, " |
| 170 | + f"got {tuple(target.shape)}." |
| 171 | + ) |
| 172 | + if not input1.is_contiguous(): |
| 173 | + input1 = input1.contiguous() |
| 174 | + if not input2.is_contiguous(): |
| 175 | + input2 = input2.contiguous() |
| 176 | + if not target.is_contiguous(): |
| 177 | + target = target.contiguous() |
| 178 | + return input1, input2, target |
| 179 | + |
| 180 | + |
| 181 | +def cosine_embedding_loss( |
| 182 | + input1: torch.Tensor, |
| 183 | + input2: torch.Tensor, |
| 184 | + target: torch.Tensor, |
| 185 | + margin: float = 0.0, |
| 186 | + reduction=1, |
| 187 | +): |
| 188 | + logger.debug("GEMS COSINE_EMBEDDING_LOSS") |
| 189 | + input1, input2, target = _check_tensors(input1, input2, target) |
| 190 | + red = _normalize_reduction(reduction) |
| 191 | + margin = float(margin) |
| 192 | + |
| 193 | + # Flatten leading dims so the last dim is D and the row count is N. |
| 194 | + x1 = input1.reshape(-1, input1.shape[-1]) |
| 195 | + x2 = input2.reshape(-1, input2.shape[-1]) |
| 196 | + y = target.reshape(-1) if target.numel() > 0 else target |
| 197 | + n_rows = x1.shape[0] |
| 198 | + d = x1.shape[1] |
| 199 | + x1_stride0 = x1.stride(0) |
| 200 | + x2_stride0 = x2.stride(0) |
| 201 | + |
| 202 | + if red == 0: |
| 203 | + # reduction = 'none' |
| 204 | + out = torch.empty((n_rows,), device=input1.device, dtype=input1.dtype) |
| 205 | + if n_rows == 0: |
| 206 | + return out |
| 207 | + BLOCK_D = triton.next_power_of_2(d) |
| 208 | + _cosine_embedding_loss_elementwise_kernel[(n_rows,)]( |
| 209 | + x1, x2, y, out, n_rows, d, x1_stride0, x2_stride0, margin, BLOCK_D=BLOCK_D |
| 210 | + ) |
| 211 | + # Restore the leading shape (target shape). |
| 212 | + return out.reshape(target.shape) if target.numel() != n_rows else out |
| 213 | + else: |
| 214 | + # reduction = 'mean' (1) or 'sum' (2) |
| 215 | + if n_rows == 0: |
| 216 | + # Follow PyTorch behavior: sum -> 0, mean -> NaN |
| 217 | + if red == 2: |
| 218 | + return torch.zeros((), device=input1.device, dtype=input1.dtype) |
| 219 | + else: |
| 220 | + return torch.full( |
| 221 | + (), float("nan"), device=input1.device, dtype=input1.dtype |
| 222 | + ) |
| 223 | + BLOCK_D = triton.next_power_of_2(d) |
| 224 | + # Stage 1: one program per row writes its float32 loss into mid. |
| 225 | + mid_size = n_rows |
| 226 | + block_mid = min(1024, triton.next_power_of_2(mid_size)) |
| 227 | + mid = torch.empty((mid_size,), device=input1.device, dtype=torch.float32) |
| 228 | + _cosine_embedding_loss_per_row_kernel[(n_rows,)]( |
| 229 | + x1, |
| 230 | + x2, |
| 231 | + y, |
| 232 | + mid, |
| 233 | + n_rows, |
| 234 | + d, |
| 235 | + x1_stride0, |
| 236 | + x2_stride0, |
| 237 | + margin, |
| 238 | + BLOCK_D=BLOCK_D, |
| 239 | + ) |
| 240 | + # Allocate the output directly in the input dtype and let the kernel |
| 241 | + # cast on store. This avoids any host-side arithmetic / `.to()` that |
| 242 | + # use_gems would re-dispatch to pointwise kernels. |
| 243 | + out = torch.zeros((), device=input1.device, dtype=input1.dtype) |
| 244 | + # inv_scale=1/n_rows folds the mean division into the reduction kernel; |
| 245 | + # for sum it is 1.0. |
| 246 | + inv_scale = 1.0 if red == 2 else (1.0 / float(n_rows)) |
| 247 | + _cosine_embedding_loss_final_reduce_kernel[(1,)]( |
| 248 | + mid, out, mid_size, inv_scale, BLOCK_MID=block_mid |
| 249 | + ) |
| 250 | + return out |
0 commit comments