Conversation
1e21de7 to
d8dabe8
Compare
d8dabe8 to
2e32818
Compare
2e32818 to
75ebfe3
Compare
| def default_kernel_map(self) -> Dict[str, Kernel]: | ||
| return {} | ||
|
|
||
| def _infer_output_shapes( |
There was a problem hiding this comment.
Why do these two functions use only one parameter but pass so many unused ones?
There was a problem hiding this comment.
The full signatures are intentional because they mirror the manifest input slots. _infer_output_shapes is required by the Op abstract contract and manifest parity checks; Dense GQA output shape depends only on q_shape. _validate_dtypes uses the tensor inputs to enforce Q/K/V, scale, and RoPE dtype relations.
| v_scale: Optional[torch.Tensor], | ||
| rope_cos: Optional[torch.Tensor], | ||
| rope_sin: Optional[torch.Tensor], | ||
| ) -> torch.Tensor: ... |
There was a problem hiding this comment.
A protocol feels too heavy for a single-use interface — worth it only if the interface is meant to be shared across many APIs.
There was a problem hiding this comment.
Agreed. I removed _DenseFwdCallable, the Protocol, and the cast. The Op now uses the existing get_or_build_kernel boundary directly.
lcy-seso
left a comment
There was a problem hiding this comment.
Reviewed the Dense GQA boundary. One blocking item, the rest are placement and duplication.
Blocking
- Query-to-key alignment is undefined for
S_q < S_kv(inline ongqa.py:291). It decides both the mask and the RoPE positions, and every target will guess differently until it is written down.
Also
forwardcarries 21 argument checks inline while two sibling ops in the same file use_validate_forward_inputs.- Three checks duplicate
__init__;resolved_sm_scaleis computed and never used. - No test.
grepfinds the class only inops/__init__.py,attention/__init__.py, and the manifest;source.testnamestests/ops/attention/test_gqa.py, which has no case for it.status: spec-onlycovers the missing kernel, but the 95 lines of validation ship constructible and callable, unexercised. - The class docstring never states what the op computes.
| rope_sin: Optional[torch.Tensor] = None, | ||
| ) -> torch.Tensor: | ||
| """Run Dense BSHD GQA through the selected callable.""" | ||
| batch, seq_len_q, heads, dim = q.shape |
There was a problem hiding this comment.
forward runs 26 branches; 21 of them are raise ValueError on the arguments.
- Two sibling ops in this file already extract that:
GroupedQueryAttentionPrefillVarlenFwdOp._validate_forward_inputs(L891) andGroupedQueryAttentionPrefillPagedWithKVCacheFwdOp._validate_forward_inputs(L1178). - Here only dtype is extracted (
_validate_dtypes); shape, device, and combination checks stay inline. Half-extracted reads worse than not extracting. - Suggested shape:
_validate_forward_inputs+_resolve_optional_inputsreturning the 8-tuple, leavingforwardas unpack shapes -> validate -> resolve ->self._get_callable(inputs)(*inputs).
There was a problem hiding this comment.
Done. Runtime checks now live in _validate_forward_inputs; contiguous conversion and fixed manifest-slot construction live in _canonicalize_inputs. forward is now validate -> canonicalize -> get kernel -> invoke.
| ) -> torch.Tensor: | ||
| """Run Dense BSHD GQA through the selected callable.""" | ||
| batch, seq_len_q, heads, dim = q.shape | ||
| batch_kv, seq_len_kv, heads_kv, dim_kv = k.shape |
There was a problem hiding this comment.
No ndim check on q / k / v.
- A 3-D
qfails here asValueError: not enough values to unpack, which does not name the argument. - Both sibling
_validate_forward_inputsmethods check ndim explicitly.
There was a problem hiding this comment.
Done. q, k, and v now receive explicit rank-4 BSHD checks before shape unpacking, with the argument name in each error.
| _validate_gqa_dims(heads, heads_kv, dim) | ||
|
|
||
| if self.is_causal and seq_len_q > seq_len_kv: | ||
| raise ValueError("causal dense attention requires seq_len_q <= seq_len_kv") |
There was a problem hiding this comment.
Blocking: seq_len_q < seq_len_kv is admitted, but nothing defines where query i sits on the key axis.
- The existing square kernel masks with
q_idx >= k_idx, i.e. top-left (kernels/attention/gqa_fwd.py:102). - Decode needs bottom-right (
i + S_kv - S_q >= j), otherwise it attends to key 0 only. - The same offset decides which RoPE position
q_igets, and there is no position/offset input. - Neither the docstring nor
shape_rulesstates it, so every target will pick its own. Please fix the alignment here.
There was a problem hiding this comment.
Fixed. The class contract now defines bottom-right alignment as p_i = i + S_kv - S_q; causal/window masking and fused-RoPE query positions are defined from p_i. The manifest records the same constraints.
| if self.pos_encoding_mode == "rope" and seq_len_q > seq_len_kv: | ||
| raise ValueError("fused RoPE requires seq_len_q <= seq_len_kv") | ||
|
|
||
| resolved_sm_scale = _attention_scale(dim, self.sm_scale) |
There was a problem hiding this comment.
resolved_sm_scale is computed and discarded.
- Its only use is the
isfinitecheck on the next line, which__init__:183already performs. _manifest_params()hands the builderself.sm_scale, possiblyNone, so every target re-derives1/sqrt(D).softcapis handled the other way: normalized once in__init__via_score_softcap. Pick one convention for both.
There was a problem hiding this comment.
The discarded local has been removed. sm_scale=None remains constructor state because its default depends on the current head dimension; softcap is shape-independent and is normalized at construction. The future BUILTIN build closure (or a target builder using TensorSpec) resolves the dimension-dependent scale.
| if not math.isfinite(resolved_sm_scale): | ||
| raise ValueError("sm_scale must be finite") | ||
| if self.pos_encoding_mode not in ("none", "rope"): | ||
| raise ValueError("pos_encoding_mode must be 'none' or 'rope'") |
There was a problem hiding this comment.
Duplicate of __init__:179. pos_encoding_mode is not mutated between construction and call.
There was a problem hiding this comment.
Done. pos_encoding_mode is validated only at construction now.
| if tuple(scale.shape) != (batch, heads_kv): | ||
| raise ValueError(f"{name} must have shape {(batch, heads_kv)}") | ||
| normalized_scales.append(scale.contiguous()) | ||
| resolved_scales = tuple(normalized_scales) if normalized_scales else (None, None, None) |
There was a problem hiding this comment.
normalized_scales is built by append, so its length depends on the all-or-none check at L314 holding.
- If that check moves, this splats a 1- or 2-tuple into
inputsat L359 and the failure surfaces at the callable, not at validation. tuple(s.contiguous() if s is not None else None for s in scales)removes the coupling.
There was a problem hiding this comment.
Done. _canonicalize_inputs now constructs all eight manifest slots with one tuple comprehension, preserving None independently of the all-or-none validation.
| raise ValueError(f"{name} must have dtype {output_dtype}") | ||
|
|
||
| def eval_roofline(self) -> tuple[int, int]: | ||
| raise NotImplementedError("Dense GQA has no in-tree implementation yet") |
There was a problem hiding this comment.
This override can go: Op.eval_roofline (op_base.py:191) already raises NotImplementedError, with a message pointing at roofline.md 4.4.6.
There was a problem hiding this comment.
I retained this minimal override after checking the class contract: Op.eval_roofline is abstract, and roofline codegen intentionally skips status: spec-only. Removing the override makes the public class non-instantiable. The override can disappear once an implemented entry supplies generated roofline code.
| selection and caching of concrete kernels belongs here, not in the Op. | ||
| """ | ||
|
|
||
| def __call__( |
There was a problem hiding this comment.
_DenseFwdCallable has one use site, and it is the target of a cast, which does not check at runtime.
As written it buys what a Callable[...] alias would. Worth a Protocol only if a target-side implementation gets checked against it.
There was a problem hiding this comment.
Agreed and removed. There is no dedicated callable type or second callable-owned cache now.
| FP16/BF16 and scaled FP8 inputs, and optional caller-owned RoPE tables. | ||
| This target-neutral layer caches a target-owned :class:`_DenseFwdCallable` | ||
| under the standard input-signature rules; concrete kernel dispatch remains | ||
| inside that callable. The in-tree callable is deferred to a follow-up. |
There was a problem hiding this comment.
This class defines the public contract for dense GQA but never states what it computes. Please write the linear algebra into the class docstring.
The docs site renders LaTeX now; ops/norm/layer_norm.py:22 is the in-repo precedent for the format ($$ ... $$, escaped backslashes). The equation should pin down what prose leaves open:
- which KV group head
hreads - where query
isits on the key axis, i.e. the offset the causal and window masks compare against (see the alignment comment on L291) - the score scale and its default
- how
softcapenters - where the FP8
q_scale/k_scale/v_scalefactors apply - what accumulates in FP32, and where the cast to
dtypehappens
There was a problem hiding this comment.
Done. The class docstring now specifies KV-group head mapping, bottom-right coordinates, mask/window semantics, scale default, softcap placement, FP8 scale placement, FP32 accumulation, and output casting.
| - {q: bfloat16, k: bfloat16, v: bfloat16, o: bfloat16} | ||
| - {q: float8_e4m3fn, k: float8_e4m3fn, v: float8_e4m3fn, o: float16} | ||
| - {q: float8_e4m3fn, k: float8_e4m3fn, v: float8_e4m3fn, o: bfloat16} | ||
| shape_rules: |
There was a problem hiding this comment.
forward enforces two constraints missing from shape_rules:
is_causalrequiresS_q <= S_kv(gqa.py:291)pos_encoding_mode == 'rope'requiresS_q <= S_kv(gqa.py:293)
The manifest is the spec, so both belong here, plus the rule stating query-to-key alignment once it is decided.
There was a problem hiding this comment.
Done. The manifest now includes the causal and fused-RoPE S_q <= S_kv rules and documents bottom-right query-to-key alignment.
|
Updated at
I retained the minimal The PR has also been narrowed: it no longer changes |
## Problems - `GroupedQueryAttentionDenseFwdOp` (tile-ai/TileOPs#1975) has docstring formulas but no `:::` entry on `api/attention.md`, so the op and its math never render. - A scan of TileOPs top-level exports and manifest entries against the API pages found 24 public ops in the same state. - `api/quantization.md` and `api/topk.md` are coming-soon placeholders while both ops are `implemented` in the manifest and exported at the top level. - The RoPE, pooling, convolution, and dropout families have no API page at all. ## Changes - `attention.md`: add `GroupedQueryAttentionDenseFwdOp` under Grouped-query attention, and an Attention indexing section for `FP8LightningIndexerFwdOp`. - `quantization.md`, `topk.md`: replace the placeholders with `FP8QuantFwdOp` and `TopkSelectorFwdOp`. - New pages: `rope.md` (6 ops), `pool.md` (12 ops), `convolution.md` (3 ops), `dropout.md` (1 op), each following the existing page pattern. - `mkdocs.yml`: four new nav entries plus `nav_translations` labels; `api/index.md`: table rows for the new pages, stale-interface caveats dropped. - Verified: `mkdocs build` renders all six pages; the Dense GQA $$ formulas arrive as arithmatex blocks. 6 new griffe warnings are missing type annotations in TileOPs docstrings (`fp8_quant.py`, `topk_selector.py`, `fp8_lightning_indexer.py`), same class as the 16 pre-existing ones — the fix belongs upstream. ## Left out on purpose - MoE and Engram ops: the MoE family is mid-refactor with spec-only staged ops, and neither family is exported at the `tileops.ops` top level. - `MeanPoolingForwardOp`: an `UnmanifestedOp`.
## Summary - document that Dense GQA derives `B`, `Sq`, `Skv`, `H`, `Hkv`, `D`, and input dtype from every `forward` call - separate construction-time attention semantics from call-time tensor metadata - define the full computation: FP8 dequantization, optional RoPE, scaled QK, softcap, causal/window masking, softmax, and PV reduction - map every constructor parameter to its effect in the equations - clarify that one Op instance may resolve and cache implementations for multiple input shapes ## Scope This draft does **not** change the constructor, `forward` signature, manifest, validation, dispatch, or kernels. It only clarifies the input-inferred contract established in #1975, following the documentation structure used by `GemmFwdOp`.
## Summary - Define the spec-only public boundary for `GroupedQueryAttentionDenseFwdOp` over dense BSHD tensors. - Standardize rectangular causal/window/RoPE semantics on bottom-right alignment. - Keep validation and optional-input normalization in the Op, then resolve implementations through the existing `Op.get_or_build_kernel` boundary. A future BUILTIN PR will provide the shape/dtype key and concrete kernel construction.
Summary
GroupedQueryAttentionDenseFwdOpover dense BSHD tensors.Op.get_or_build_kernelboundary. A future BUILTIN PR will provide the shape/dtype key and concrete kernel construction.The ABI covers rectangular prefill and contiguous decode, FP16/BF16, scaled FP8, causal/window/softcap, and NeoX/interleaved RoPE. This PR adds no BUILTIN kernel, benchmark, compatibility adapter, design-document change,
Opbase change, runtime test, ortorch.compilepromise; kernel families and their public execution tests will migrate separately under #1916.Validation
git diff --checkpassed