diff --git a/tileops/manifest/attention_indexing.yaml b/tileops/manifest/attention_indexing.yaml new file mode 100644 index 000000000..0074ce322 --- /dev/null +++ b/tileops/manifest/attention_indexing.yaml @@ -0,0 +1,94 @@ +# Attention indexing and sparse selection operators. + +FP8LightingIndexerOp: + ref_api: "none" + family: attention_indexing + status: implemented + + signature: + inputs: + index_q: {dtype: "bfloat16 | float8_e4m3fn", shape: "[batch, seq_len, heads, index_dim]"} + index_k: {dtype: "bfloat16 | float8_e4m3fn", shape: "[batch, seq_len_kv, kv_group, index_dim]"} + weights: {dtype: "float32", shape: "[seq_len, heads]"} + cu_seqlen_ks: {dtype: "int32", shape: "[seq_len]"} + cu_seqlen_ke: {dtype: "int32", shape: "[seq_len]"} + index_k_scale: {dtype: "float32", shape: "[batch, seq_len_kv, kv_group]"} + outputs: + logits: {dtype: "float32", shape: "[batch, seq_len, seq_len_kv, kv_group]"} + params: + batch: {type: int} + seq_len: {type: int} + heads: {type: int} + index_dim: {type: int} + seq_len_kv: {type: int} + kv_group: {type: int} + clean_logits: {type: bool, default: true} + config: {type: "dict | None", default: null} + dtype_combos: + - {index_q: bfloat16, index_k: bfloat16, weights: float32, cu_seqlen_ks: int32, cu_seqlen_ke: int32, index_k_scale: float32} + - {index_q: float8_e4m3fn, index_k: float8_e4m3fn, weights: float32, cu_seqlen_ks: int32, cu_seqlen_ke: int32, index_k_scale: float32} + shape_rules: + - "index_q.shape == (batch, seq_len, heads, index_dim)" + - "index_k.shape == (batch, seq_len_kv, kv_group, index_dim)" + - "weights.shape == (seq_len, heads)" + - "cu_seqlen_ks.shape == (seq_len,)" + - "cu_seqlen_ke.shape == (seq_len,)" + - "index_k_scale.shape == (batch, seq_len_kv, kv_group)" + + workloads: + - {batch: 1, seq_len: 8192, heads: 32, index_dim: 64, seq_len_kv: 8192, kv_group: 1, clean_logits: true, dtypes: [bfloat16], label: "lighting-indexer-bf16-s8k-h32-d64"} + - {batch: 1, seq_len: 8192, heads: 32, index_dim: 64, seq_len_kv: 8192, kv_group: 1, clean_logits: true, dtypes: [float8_e4m3fn], label: "lighting-indexer-fp8-s8k-h32-d64"} + + roofline: + func: "tileops.perf.formulas.fp8_lighting_indexer_roofline" + + source: + kernel: tileops/kernels/fp8_lighting_indexer.py + kernel_map: + fp8_lighting_indexer_kernel: FP8LightingIndexerKernel + op: tileops/ops/fp8_lighting_indexer.py + test: tests/ops/test_fp8_lighting_indexer.py + bench: benchmarks/ops/bench_fp8_lighting_indexer.py + bench_manifest_driven: false + +TopkSelectorOp: + ref_api: "torch.topk" + family: attention_indexing + status: implemented + + signature: + inputs: + index_score: {dtype: "float32", shape: "[batch, seq_len, seq_len_kv, kv_group]"} + starts: {dtype: "int32", shape: "[batch, seq_len]"} + ends: {dtype: "int32", shape: "[batch, seq_len]"} + outputs: + indexes: {dtype: "int32", shape: "[batch, seq_len, kv_group, topk]"} + params: + batch: {type: int} + seq_len: {type: int} + seq_len_kv: {type: int} + kv_group: {type: int} + topk: {type: int} + in_dtype: {type: torch.dtype} + out_dtype: {type: torch.dtype} + shape_rules: + - "index_score.shape == (batch, seq_len, seq_len_kv, kv_group)" + - "starts.shape == (batch, seq_len)" + - "ends.shape == (batch, seq_len)" + - "0 < topk <= seq_len_kv" + + workloads: + - {batch: 1, seq_len: 32768, seq_len_kv: 65536, kv_group: 1, topk: 1024, in_dtype: float32, out_dtype: int32, dtypes: [float32], label: "topk1024-s32k-kv64k"} + - {batch: 1, seq_len: 32768, seq_len_kv: 65536, kv_group: 1, topk: 2048, in_dtype: float32, out_dtype: int32, dtypes: [float32], label: "topk2048-s32k-kv64k"} + + roofline: + func: "tileops.perf.formulas.topk_selector_roofline" + + source: + kernel: tileops/kernels/topk_selector.py + kernel_map: + topk_selector_kernel: TopkSelectorKernel + op: tileops/ops/topk_selector.py + test: tests/ops/test_topk_selector.py + bench: benchmarks/ops/bench_topk_selector.py + bench_manifest_driven: false diff --git a/tileops/manifest/gemm.yaml b/tileops/manifest/gemm.yaml index 19c3c241d..845ff369a 100644 --- a/tileops/manifest/gemm.yaml +++ b/tileops/manifest/gemm.yaml @@ -50,3 +50,52 @@ GemmOp: kernel_map: gemm_kernel: GemmKernel gemv_kernel: GemvKernel + +GroupedGemmOp: + ref_api: "none" + family: gemm + status: implemented + + signature: + inputs: + a: {dtype: "float16 | bfloat16", shape: "[batch_sum, K] | [batch_sum, N]"} + b: {dtype: "same_as(a)", shape: "[batch_count, N, K] | [batch_count, K, N] | [K, batch_sum] | [batch_sum, K]"} + batch_sizes: {dtype: "int32", shape: "[batch_count]"} + batch_offsets: {dtype: "int32", shape: "[batch_count]"} + batch_padded_offsets: {dtype: "int32", shape: "[batch_count]"} + outputs: + output: {dtype: "same_as(a)", shape: "[batch_sum, N] | [batch_count, N, K]"} + params: + batch_sum: {type: int} + batch_count: {type: int} + n: {type: int} + k: {type: int} + transpose_a: {type: bool, default: false} + transpose_b: {type: bool, default: true} + shape_rules: + - "batch_sizes.shape == (batch_count,)" + - "batch_offsets.shape == (batch_count,)" + - "batch_padded_offsets.shape == (batch_count,)" + - "a.shape == (batch_sum, k) if not transpose_a else a.shape == (batch_sum, n)" + - "b.shape == (batch_count, n, k) if (not transpose_a and transpose_b) else True" + - "b.shape == (batch_count, k, n) if (not transpose_a and not transpose_b) else True" + - "b.shape == (k, batch_sum) if (transpose_a and transpose_b) else True" + - "b.shape == (batch_sum, k) if (transpose_a and not transpose_b) else True" + + workloads: + - {batch_sum: 4096, batch_count: 16, n: 4096, k: 4096, dtype: float16, transpose_a: false, transpose_b: true, dtypes: [float16], label: "nt-batch16-m4096-n4096-k4096-fp16"} + - {batch_sum: 4096, batch_count: 16, n: 4096, k: 4096, dtype: bfloat16, transpose_a: false, transpose_b: true, dtypes: [bfloat16], label: "nt-batch16-m4096-n4096-k4096-bf16"} + - {batch_sum: 4096, batch_count: 16, n: 4096, k: 4096, dtype: float16, transpose_a: false, transpose_b: false, dtypes: [float16], label: "nn-batch16-m4096-n4096-k4096-fp16"} + - {batch_sum: 4096, batch_count: 16, n: 4096, k: 4096, dtype: float16, transpose_a: true, transpose_b: false, dtypes: [float16], label: "tn-batch16-m4096-n4096-k4096-fp16"} + + roofline: + func: "tileops.perf.formulas.grouped_gemm_roofline" + + source: + kernel: tileops/kernels/grouped_gemm/grouped_gemm.py + kernel_map: + grouped_gemm_kernel: GroupedGemmKernel + op: tileops/ops/grouped_gemm.py + test: tests/ops/test_grouped_gemm.py + bench: benchmarks/ops/bench_grouped_gemm.py + bench_manifest_driven: false diff --git a/tileops/manifest/position_encoding.yaml b/tileops/manifest/position_encoding.yaml new file mode 100644 index 000000000..7957a2fe9 --- /dev/null +++ b/tileops/manifest/position_encoding.yaml @@ -0,0 +1,253 @@ +# Position-encoding operators. +# +# These ops already exist in tileops/ops/rope.py. The manifest keeps the +# current runtime surface as source of truth: construction binds the static +# sequence/head dimensions and forward applies the rotation to input tensors. + +RopeNeoxOp: + ref_api: "none" + family: position_encoding + status: implemented + + signature: + inputs: + x: {dtype: "float16 | bfloat16 | float32", shape: "[seq_len, head_dim] | [batch, seq_len, num_heads, head_dim]"} + outputs: + output: {dtype: "same_as(x)", shape: "same_as(x)"} + params: + seq_len: {type: int} + head_dim: {type: int} + dtype: {type: torch.dtype} + layout: {type: str, default: "1d"} + batch: {type: int, default: 1} + num_heads: {type: int, default: 1} + base: {type: float, default: 10000.0} + shape_rules: + - "x.shape == (seq_len, head_dim) or x.shape == (batch, seq_len, num_heads, head_dim)" + - "head_dim % 2 == 0" + + workloads: + - {seq_len: 2048, head_dim: 64, dtype: float16, layout: "1d", dtypes: [float16], label: "neox-1d-2k-d64-fp16"} + - {seq_len: 4096, head_dim: 128, dtype: bfloat16, layout: "1d", dtypes: [bfloat16], label: "neox-1d-4k-d128-bf16"} + - {batch: 2, seq_len: 2048, num_heads: 32, head_dim: 128, dtype: float16, layout: "2d", dtypes: [float16], label: "neox-2d-b2-s2k-h32-d128-fp16"} + + roofline: + func: "tileops.perf.formulas.rope_roofline" + + source: + kernel: tileops/kernels/rope.py + kernel_map: + rope_neox: RopeNeoxKernel + op: tileops/ops/rope.py + test: tests/ops/test_rope.py + bench: benchmarks/ops/bench_rope.py + bench_manifest_driven: false + +RopeNonNeoxOp: + ref_api: "none" + family: position_encoding + status: implemented + variant_of: RopeNeoxOp + + signature: + inputs: + x: {dtype: "float16 | bfloat16 | float32", shape: "[seq_len, head_dim] | [batch, seq_len, num_heads, head_dim]"} + outputs: + output: {dtype: "same_as(x)", shape: "same_as(x)"} + params: + seq_len: {type: int} + head_dim: {type: int} + dtype: {type: torch.dtype} + layout: {type: str, default: "1d"} + batch: {type: int, default: 1} + num_heads: {type: int, default: 1} + base: {type: float, default: 10000.0} + shape_rules: + - "x.shape == (seq_len, head_dim) or x.shape == (batch, seq_len, num_heads, head_dim)" + - "head_dim % 2 == 0" + + workloads: + - {seq_len: 2048, head_dim: 64, dtype: float16, layout: "1d", dtypes: [float16], label: "non-neox-1d-2k-d64-fp16"} + - {batch: 2, seq_len: 2048, num_heads: 32, head_dim: 128, dtype: bfloat16, layout: "2d", dtypes: [bfloat16], label: "non-neox-2d-b2-s2k-h32-d128-bf16"} + + roofline: + func: "tileops.perf.formulas.rope_roofline" + + source: + kernel: tileops/kernels/rope.py + kernel_map: + rope_non_neox: RopeNonNeoxKernel + op: tileops/ops/rope.py + test: tests/ops/test_rope.py + bench: benchmarks/ops/bench_rope.py + bench_manifest_driven: false + +RopeLlama31Op: + ref_api: "none" + family: position_encoding + status: implemented + variant_of: RopeNeoxOp + + signature: + inputs: + x: {dtype: "float16 | bfloat16 | float32", shape: "[seq_len, head_dim] | [batch, seq_len, num_heads, head_dim]"} + outputs: + output: {dtype: "same_as(x)", shape: "same_as(x)"} + params: + seq_len: {type: int} + head_dim: {type: int} + dtype: {type: torch.dtype} + layout: {type: str, default: "1d"} + batch: {type: int, default: 1} + num_heads: {type: int, default: 1} + base: {type: float, default: 10000.0} + scale_factor: {type: float, default: 8.0} + low_freq_factor: {type: float, default: 1.0} + high_freq_factor: {type: float, default: 4.0} + original_max_position: {type: int, default: 8192} + shape_rules: + - "x.shape == (seq_len, head_dim) or x.shape == (batch, seq_len, num_heads, head_dim)" + - "head_dim % 2 == 0" + + workloads: + - {seq_len: 8192, head_dim: 128, dtype: bfloat16, layout: "1d", dtypes: [bfloat16], label: "llama31-1d-8k-d128-bf16"} + - {batch: 1, seq_len: 8192, num_heads: 32, head_dim: 128, dtype: float16, layout: "2d", dtypes: [float16], label: "llama31-2d-b1-s8k-h32-d128-fp16"} + + roofline: + func: "tileops.perf.formulas.rope_roofline" + + source: + kernel: tileops/kernels/rope.py + kernel_map: + rope_llama31: RopeLlama31Kernel + op: tileops/ops/rope.py + test: tests/ops/test_rope.py + bench: benchmarks/ops/bench_rope.py + bench_manifest_driven: false + +RopeYarnOp: + ref_api: "none" + family: position_encoding + status: implemented + variant_of: RopeNeoxOp + + signature: + inputs: + x: {dtype: "float16 | bfloat16 | float32", shape: "[seq_len, head_dim] | [batch, seq_len, num_heads, head_dim]"} + outputs: + output: {dtype: "same_as(x)", shape: "same_as(x)"} + params: + seq_len: {type: int} + head_dim: {type: int} + dtype: {type: torch.dtype} + layout: {type: str, default: "1d"} + batch: {type: int, default: 1} + num_heads: {type: int, default: 1} + base: {type: float, default: 10000.0} + scale: {type: float, default: 16.0} + original_max_position: {type: int, default: 4096} + beta_fast: {type: float, default: 32.0} + beta_slow: {type: float, default: 1.0} + attn_factor: {type: float, default: 1.0} + shape_rules: + - "x.shape == (seq_len, head_dim) or x.shape == (batch, seq_len, num_heads, head_dim)" + - "head_dim % 2 == 0" + + workloads: + - {seq_len: 8192, head_dim: 128, dtype: bfloat16, layout: "1d", dtypes: [bfloat16], label: "yarn-1d-8k-d128-bf16"} + - {batch: 1, seq_len: 8192, num_heads: 32, head_dim: 128, dtype: float16, layout: "2d", dtypes: [float16], label: "yarn-2d-b1-s8k-h32-d128-fp16"} + + roofline: + func: "tileops.perf.formulas.rope_roofline" + + source: + kernel: tileops/kernels/rope.py + kernel_map: + rope_yarn: RopeYarnKernel + op: tileops/ops/rope.py + test: tests/ops/test_rope.py + bench: benchmarks/ops/bench_rope.py + bench_manifest_driven: false + +RopeLongRopeOp: + ref_api: "none" + family: position_encoding + status: implemented + variant_of: RopeNeoxOp + + signature: + inputs: + x: {dtype: "float16 | bfloat16 | float32", shape: "[seq_len, head_dim] | [batch, seq_len, num_heads, head_dim]"} + outputs: + output: {dtype: "same_as(x)", shape: "same_as(x)"} + params: + seq_len: {type: int} + head_dim: {type: int} + dtype: {type: torch.dtype} + layout: {type: str, default: "1d"} + batch: {type: int, default: 1} + num_heads: {type: int, default: 1} + base: {type: float, default: 10000.0} + rescale_factors: {type: "torch.Tensor | None", default: null} + max_position_embeddings: {type: int, default: 4096} + original_max_position_embeddings: {type: int, default: 4096} + shape_rules: + - "x.shape == (seq_len, head_dim) or x.shape == (batch, seq_len, num_heads, head_dim)" + - "head_dim % 2 == 0" + + workloads: + - {seq_len: 8192, head_dim: 128, dtype: bfloat16, layout: "1d", dtypes: [bfloat16], label: "longrope-1d-8k-d128-bf16"} + - {batch: 1, seq_len: 8192, num_heads: 32, head_dim: 128, dtype: float16, layout: "2d", dtypes: [float16], label: "longrope-2d-b1-s8k-h32-d128-fp16"} + + roofline: + func: "tileops.perf.formulas.rope_roofline" + + source: + kernel: tileops/kernels/rope.py + kernel_map: + rope_longrope: RopeLongRopeKernel + op: tileops/ops/rope.py + test: tests/ops/test_rope.py + bench: benchmarks/ops/bench_rope.py + bench_manifest_driven: false + +RopeNeoxPositionIdsOp: + ref_api: "none" + family: position_encoding + status: implemented + + signature: + inputs: + x: {dtype: "float16 | bfloat16 | float32", shape: "[num_tokens, num_heads, head_dim]"} + position_ids: {dtype: "int32 | int64", shape: "[num_tokens]"} + outputs: + output: {dtype: "same_as(x)", shape: "[num_tokens, num_heads, head_dim]"} + params: + num_tokens: {type: int} + num_heads: {type: int} + head_dim: {type: int} + max_position: {type: int} + dtype: {type: torch.dtype} + base: {type: float, default: 10000.0} + rotary_dim: {type: "int | None", default: null} + shape_rules: + - "x.shape == (num_tokens, num_heads, head_dim)" + - "position_ids.shape == (num_tokens,)" + - "head_dim % 2 == 0" + - "rotary_dim is None or (rotary_dim > 0 and rotary_dim % 2 == 0 and rotary_dim <= head_dim)" + + workloads: + - {num_tokens: 2048, num_heads: 32, head_dim: 128, max_position: 4096, dtype: float16, dtypes: [float16], label: "position-ids-s2k-h32-d128-fp16"} + - {num_tokens: 4096, num_heads: 32, head_dim: 128, max_position: 8192, dtype: bfloat16, dtypes: [bfloat16], label: "position-ids-s4k-h32-d128-bf16"} + + roofline: + func: "tileops.perf.formulas.rope_position_ids_roofline" + + source: + kernel: tileops/kernels/rope.py + kernel_map: + rope_neox_position_ids: RopeNeoxPositionIdsKernel + op: tileops/ops/rope.py + test: tests/ops/test_rope.py + bench: benchmarks/ops/bench_rope.py + bench_manifest_driven: false diff --git a/tileops/manifest/quantization.yaml b/tileops/manifest/quantization.yaml new file mode 100644 index 000000000..3d99dc941 --- /dev/null +++ b/tileops/manifest/quantization.yaml @@ -0,0 +1,42 @@ +# Narrow quantization helper operators. +# +# These entries describe existing helper surfaces only. They do not claim the +# full INT8/INT4/NF4/FP8 quantization family from the older release plan. + +FP8QuantOp: + ref_api: "none" + family: quantization + status: implemented + + signature: + inputs: + input_tensor: {dtype: "float16 | bfloat16 | float32", shape: "[batch, seq_len_kv, kv_group, index_dim]"} + outputs: + scale_tensor: {dtype: "float32", shape: "[batch, seq_len_kv, kv_group]"} + output_tensor: {dtype: "float8_e4m3fn", shape: "[batch, seq_len_kv, kv_group, index_dim]"} + params: + batch: {type: int} + seq_len_kv: {type: int} + kv_group: {type: int} + index_dim: {type: int} + in_dtype: {type: torch.dtype} + shape_rules: + - "input_tensor.shape == (batch, seq_len_kv, kv_group, index_dim)" + - "index_dim > 0" + + workloads: + - {batch: 1, seq_len_kv: 8192, kv_group: 1, index_dim: 64, in_dtype: float16, dtypes: [float16], label: "kv-index-8k-d64-fp16"} + - {batch: 1, seq_len_kv: 8192, kv_group: 1, index_dim: 64, in_dtype: bfloat16, dtypes: [bfloat16], label: "kv-index-8k-d64-bf16"} + - {batch: 1, seq_len_kv: 4096, kv_group: 1, index_dim: 128, in_dtype: float32, dtypes: [float32], label: "kv-index-4k-d128-fp32"} + + roofline: + func: "tileops.perf.formulas.fp8_quant_roofline" + + source: + kernel: tileops/kernels/fp8_quant.py + kernel_map: + fp8_quant_kernel: FP8QuantKernel + op: tileops/ops/fp8_quant.py + test: tests/ops/test_fp8_quant.py + bench: benchmarks/ops/bench_fp8_quant.py + bench_manifest_driven: false diff --git a/tileops/manifest/regularization.yaml b/tileops/manifest/regularization.yaml new file mode 100644 index 000000000..53092fed9 --- /dev/null +++ b/tileops/manifest/regularization.yaml @@ -0,0 +1,40 @@ +# Regularization operators. + +DropoutOp: + ref_api: "torch.nn.functional.dropout" + family: regularization + status: implemented + + signature: + inputs: + x: {dtype: "float16 | bfloat16 | float32"} + outputs: + output: {dtype: "same_as(x)"} + params: + N_total: {type: int} + dtype: {type: torch.dtype} + p: {type: float, default: 0.5} + seed: {type: int, default: 0} + training: {type: bool, default: true} + shape_rules: + - "N_total > 0" + - "0.0 <= p <= 1.0" + + workloads: + # Normal kernel path. p=0, p=1, and training=false are short-circuit paths + # and should be tested separately from release-facing roofline workloads. + - {N_total: 4194304, dtype: float16, p: 0.5, training: true, dtypes: [float16], label: "tokens-1k-hidden-4k-fp16"} + - {N_total: 10485760, dtype: bfloat16, p: 0.5, training: true, dtypes: [bfloat16], label: "tokens-1k-hidden-10k-bf16"} + - {N_total: 4194304, dtype: float32, p: 0.1, training: true, dtypes: [float32], label: "tokens-1k-hidden-4k-fp32-p01"} + + roofline: + func: "tileops.perf.formulas.dropout_roofline" + + source: + kernel: tileops/kernels/dropout.py + kernel_map: + dropout: DropoutKernel + op: tileops/ops/dropout.py + test: tests/ops/test_dropout.py + bench: benchmarks/ops/bench_dropout.py + bench_manifest_driven: false diff --git a/tileops/manifest/sequence_modeling.yaml b/tileops/manifest/sequence_modeling.yaml new file mode 100644 index 000000000..3b5d202f1 --- /dev/null +++ b/tileops/manifest/sequence_modeling.yaml @@ -0,0 +1,284 @@ +# Miscellaneous mature sequence-modeling operators with existing runtime, +# tests, and benchmarks. + +EngramGateConvFwdOp: + ref_api: "none" + family: sequence_modeling + status: implemented + + signature: + inputs: + H: {dtype: "float16 | bfloat16", shape: "[M, seq_len, d]"} + k: {dtype: "same_as(H)", shape: "[M, seq_len, d]"} + v: {dtype: "same_as(H)", shape: "[M, seq_len, d]"} + rms_w_h: {dtype: "same_as(H)", shape: "[d]"} + rms_w_v: {dtype: "same_as(H)", shape: "[d]"} + conv_w: {dtype: "same_as(H)", shape: "[4, d]"} + outputs: + Y: {dtype: "same_as(H)", shape: "[M, seq_len, d]"} + vhat: {dtype: "same_as(H)", shape: "[M, seq_len, d]"} + alpha: {dtype: "float32", shape: "[M, seq_len]"} + rrms_h: {dtype: "float32", shape: "[M, seq_len]"} + rrms_k: {dtype: "float32", shape: "[M, seq_len]"} + rrms_v: {dtype: "float32", shape: "[M, seq_len]"} + params: + M: {type: int} + seq_len: {type: int} + d: {type: int} + dtype: {type: torch.dtype} + eps: {type: float, default: 1.0e-6} + shape_rules: + - "H.shape == (M, seq_len, d)" + - "k.shape == H.shape" + - "v.shape == H.shape" + - "rms_w_h.shape == (d,)" + - "rms_w_v.shape == (d,)" + - "conv_w.shape == (4, d)" + + workloads: + # d is kept 256-aligned so runtime padding does not distort roofline. + - {M: 1, seq_len: 32, d: 256, dtype: float16, dtypes: [float16], label: "fwd-b1-s32-d256-fp16"} + - {M: 2, seq_len: 64, d: 512, dtype: float16, dtypes: [float16], label: "fwd-b2-s64-d512-fp16"} + - {M: 1, seq_len: 128, d: 256, dtype: bfloat16, dtypes: [bfloat16], label: "fwd-b1-s128-d256-bf16"} + + roofline: + func: "tileops.perf.formulas.engram_gate_conv_fwd_roofline" + + source: + kernel: tileops/kernels/engram/engram_fwd.py + kernel_map: + engram_gate_conv_fwd: EngramGateConvFwdKernel + op: tileops/ops/engram.py + test: tests/ops/test_engram.py + bench: benchmarks/ops/bench_engram.py + bench_manifest_driven: false + +EngramGateConvBwdOp: + ref_api: "none" + family: sequence_modeling + status: implemented + + signature: + inputs: + dY: {dtype: "float16 | bfloat16", shape: "[M, seq_len, d]"} + H: {dtype: "same_as(dY)", shape: "[M, seq_len, d]"} + k: {dtype: "same_as(dY)", shape: "[M, seq_len, d]"} + v: {dtype: "same_as(dY)", shape: "[M, seq_len, d]"} + rms_w_h: {dtype: "same_as(dY)", shape: "[d]"} + rms_w_v: {dtype: "same_as(dY)", shape: "[d]"} + conv_w: {dtype: "same_as(dY)", shape: "[4, d]"} + vhat: {dtype: "same_as(dY)", shape: "[M, seq_len, d]"} + alpha: {dtype: "float32", shape: "[M, seq_len]"} + rrms_h: {dtype: "float32", shape: "[M, seq_len]"} + rrms_k: {dtype: "float32", shape: "[M, seq_len]"} + rrms_v: {dtype: "float32", shape: "[M, seq_len]"} + outputs: + dH: {dtype: "same_as(dY)", shape: "[M, seq_len, d]"} + dk: {dtype: "same_as(dY)", shape: "[M, seq_len, d]"} + dv: {dtype: "same_as(dY)", shape: "[M, seq_len, d]"} + drms_w_h: {dtype: "float32", shape: "[d]"} + drms_w_v: {dtype: "float32", shape: "[d]"} + dconv_w: {dtype: "float32", shape: "[4, d]"} + params: + M: {type: int} + seq_len: {type: int} + d: {type: int} + dtype: {type: torch.dtype} + eps: {type: float, default: 1.0e-6} + shape_rules: + - "dY.shape == (M, seq_len, d)" + - "H.shape == dY.shape" + - "k.shape == dY.shape" + - "v.shape == dY.shape" + - "vhat.shape == dY.shape" + - "alpha.shape == (M, seq_len)" + - "rrms_h.shape == (M, seq_len)" + - "rrms_k.shape == (M, seq_len)" + - "rrms_v.shape == (M, seq_len)" + - "conv_w.shape == (4, d)" + + workloads: + - {M: 1, seq_len: 32, d: 256, dtype: float16, dtypes: [float16], label: "bwd-b1-s32-d256-fp16"} + - {M: 2, seq_len: 64, d: 512, dtype: float16, dtypes: [float16], label: "bwd-b2-s64-d512-fp16"} + - {M: 1, seq_len: 128, d: 256, dtype: bfloat16, dtypes: [bfloat16], label: "bwd-b1-s128-d256-bf16"} + + roofline: + func: "tileops.perf.formulas.engram_gate_conv_bwd_roofline" + + source: + kernel: tileops/kernels/engram/engram_bwd.py + kernel_map: + engram_gate_conv_bwd: EngramGateConvBwdKernel + op: tileops/ops/engram.py + test: tests/ops/test_engram.py + bench: benchmarks/ops/bench_engram.py + bench_manifest_driven: false + +EngramDecodeOp: + ref_api: "none" + family: sequence_modeling + status: implemented + + signature: + inputs: + e_t: {dtype: "float16 | bfloat16", shape: "[batch, d_mem]"} + h_t: {dtype: "same_as(e_t)", shape: "[batch, d]"} + conv_state: {dtype: "same_as(e_t)", shape: "[batch, max_conv_len, d]"} + W_K: {dtype: "same_as(e_t)", shape: "[d_mem, d]"} + W_V: {dtype: "same_as(e_t)", shape: "[d_mem, d]"} + rms_w_h: {dtype: "same_as(e_t)", shape: "[d]"} + rms_w_v: {dtype: "same_as(e_t)", shape: "[d]"} + conv_w: {dtype: "same_as(e_t)", shape: "[conv_kernel_size, d]"} + outputs: + y_t: {dtype: "same_as(e_t)", shape: "[batch, d]"} + new_conv_state: {dtype: "same_as(e_t)", shape: "[batch, max_conv_len, d]"} + params: + batch: {type: int} + d_mem: {type: int} + d: {type: int} + max_conv_len: {type: int} + conv_kernel_size: {type: int} + dilation: {type: int} + dtype: {type: torch.dtype} + eps: {type: float, default: 1.0e-6} + shape_rules: + - "e_t.shape == (batch, d_mem)" + - "h_t.shape == (batch, d)" + - "conv_state.shape == (batch, max_conv_len, d)" + - "W_K.shape == (d_mem, d)" + - "W_V.shape == (d_mem, d)" + - "conv_w.shape == (conv_kernel_size, d)" + - "d % 256 == 0" + + workloads: + - {batch: 1, d_mem: 512, d: 256, max_conv_len: 12, conv_kernel_size: 4, dilation: 3, dtype: float16, dtypes: [float16], label: "decode-b1-dmem512-d256-fp16"} + - {batch: 4, d_mem: 1024, d: 512, max_conv_len: 20, conv_kernel_size: 4, dilation: 5, dtype: float16, dtypes: [float16], label: "decode-b4-dmem1024-d512-fp16"} + - {batch: 8, d_mem: 512, d: 256, max_conv_len: 18, conv_kernel_size: 4, dilation: 3, dtype: bfloat16, dtypes: [bfloat16], label: "decode-b8-dmem512-d256-bf16"} + + roofline: + func: "tileops.perf.formulas.engram_decode_roofline" + + source: + kernel: tileops/kernels/engram/engram_decode.py + kernel_map: + engram_decode: EngramDecodeKernel + op: tileops/ops/engram_decode.py + test: tests/ops/test_engram.py + bench: benchmarks/ops/bench_engram.py + bench_manifest_driven: false + +FFTC2COp: + ref_api: "torch.fft.fft" + family: spectral + status: implemented + + signature: + inputs: + x: {dtype: "complex64 | complex128", shape: "[..., n]"} + outputs: + output: {dtype: "same_as(x)", shape: "same_as(x)"} + params: + n: {type: int} + shape_rules: + - "x.shape[-1] == n" + - "n > 0" + - "n & (n - 1) == 0" + + workloads: + - {n: 4096, dtype: complex64, batch_shape: [], dtypes: [complex64], label: "fft-4k-c64-unbatched"} + - {n: 4096, dtype: complex64, batch_shape: [64], dtypes: [complex64], label: "fft-4k-c64-b64"} + - {n: 4096, dtype: complex128, batch_shape: [64], dtypes: [complex128], label: "fft-4k-c128-b64"} + + roofline: + func: "tileops.perf.formulas.fft_c2c_roofline" + + source: + kernel: tileops/kernels/fft.py + kernel_map: + fft_c2c_kernel: FFTC2CKernel + op: tileops/ops/fft.py + test: tests/ops/test_fft.py + bench: benchmarks/ops/bench_fft.py + bench_manifest_driven: false + +MHCPreOp: + ref_api: "none" + family: sequence_modeling + status: implemented + + signature: + inputs: + phi: {dtype: "float32", shape: "[n_expand * c_x, n_expand * n_expand + 2 * n_expand]"} + x: {dtype: "bfloat16", shape: "[batch, n_expand * c_x]"} + b: {dtype: "float32", shape: "[n_expand * n_expand + 2 * n_expand]"} + outputs: + x_res: {dtype: "bfloat16", shape: "[batch, n_expand * c_x]"} + x_layer: {dtype: "bfloat16", shape: "[batch, c_x]"} + params: + batch: {type: int} + n_expand: {type: int} + c_x: {type: int} + alpha_pre: {type: float} + alpha_post: {type: float} + alpha_res: {type: float} + sinkhorn_repeat: {type: int} + sinkhorn_eps: {type: float, default: 0.02} + shape_rules: + - "phi.shape == (n_expand * c_x, n_expand * n_expand + 2 * n_expand)" + - "x.shape == (batch, n_expand * c_x)" + - "b.shape == (n_expand * n_expand + 2 * n_expand,)" + + workloads: + - {batch: 1, n_expand: 4, c_x: 1280, alpha_pre: 1.0, alpha_post: 1.0, alpha_res: 1.0, sinkhorn_repeat: 1, dtype: bfloat16, dtypes: [bfloat16], label: "pre-small"} + - {batch: 2, n_expand: 4, c_x: 1920, alpha_pre: 1.0, alpha_post: 1.0, alpha_res: 1.0, sinkhorn_repeat: 1, dtype: bfloat16, dtypes: [bfloat16], label: "pre-medium"} + - {batch: 4, n_expand: 4, c_x: 2560, alpha_pre: 1.0, alpha_post: 1.0, alpha_res: 1.0, sinkhorn_repeat: 1, dtype: bfloat16, dtypes: [bfloat16], label: "pre-large"} + + roofline: + func: "tileops.perf.formulas.mhc_pre_roofline" + + source: + kernel: tileops/kernels/mhc/mhc_pre.py + kernel_map: + mhc_pre_kernel: MHCPreKernel + op: tileops/ops/mhc.py + test: tests/ops/test_mhc_pre.py + bench: benchmarks/ops/bench_mhc_pre.py + bench_manifest_driven: false + +MHCPostOp: + ref_api: "none" + family: sequence_modeling + status: implemented + + signature: + inputs: + x_layer_out: {dtype: "bfloat16", shape: "[batch, c_x]"} + h_post: {dtype: "float32", shape: "[batch, n_expand]"} + x_res: {dtype: "bfloat16", shape: "[batch, n_expand * c_x]"} + outputs: + x_out: {dtype: "bfloat16", shape: "[batch, n_expand * c_x]"} + params: + batch: {type: int} + n_expand: {type: int} + c_x: {type: int} + shape_rules: + - "x_layer_out.shape == (batch, c_x)" + - "h_post.shape == (batch, n_expand)" + - "x_res.shape == (batch, n_expand * c_x)" + + workloads: + - {batch: 1, n_expand: 4, c_x: 1280, dtype: bfloat16, dtypes: [bfloat16], label: "post-small"} + - {batch: 2, n_expand: 4, c_x: 1920, dtype: bfloat16, dtypes: [bfloat16], label: "post-medium"} + - {batch: 4, n_expand: 4, c_x: 2560, dtype: bfloat16, dtypes: [bfloat16], label: "post-large"} + + roofline: + func: "tileops.perf.formulas.mhc_post_roofline" + + source: + kernel: tileops/kernels/mhc/mhc_post.py + kernel_map: + mhc_post_kernel: MHCPostKernel + op: tileops/ops/mhc.py + test: tests/ops/test_mhc_post.py + bench: benchmarks/ops/bench_mhc_post.py + bench_manifest_driven: false diff --git a/tileops/perf/formulas.py b/tileops/perf/formulas.py index 863838722..302d50bfa 100644 --- a/tileops/perf/formulas.py +++ b/tileops/perf/formulas.py @@ -29,8 +29,15 @@ "deepseek_dsa_decode_roofline", "deepseek_mla_decode_roofline", "div_fwd_roofline", + "dropout_roofline", + "engram_decode_roofline", + "engram_gate_conv_bwd_roofline", + "engram_gate_conv_fwd_roofline", "eq_fwd_roofline", + "fft_c2c_roofline", "floor_divide_fwd_roofline", + "fp8_lighting_indexer_roofline", + "fp8_quant_roofline", "fused_moe_fwd_bytes", "ge_fwd_roofline", "gemm_fwd_roofline", @@ -45,6 +52,7 @@ "gqa_prefill_with_kv_cache_fwd_roofline", "gqa_sliding_window_fwd_roofline", "gqa_sliding_window_varlen_fwd_roofline", + "grouped_gemm_roofline", "gt_fwd_roofline", "le_fwd_roofline", "lerp_fwd_roofline", @@ -58,12 +66,17 @@ "mha_decode_paged_roofline", "mha_decode_roofline", "mha_fwd_roofline", + "mhc_post_roofline", + "mhc_pre_roofline", "minimum_fwd_roofline", "mul_fwd_roofline", "ne_fwd_roofline", "pow_fwd_roofline", "remainder_fwd_roofline", + "rope_position_ids_roofline", + "rope_roofline", "sub_fwd_roofline", + "topk_selector_roofline", "where_fwd_roofline", ] @@ -162,6 +175,10 @@ def _dtype_itemsize(dtype: Any) -> int: if hasattr(dtype, "itemsize"): return int(dtype.itemsize) dtype_name = str(dtype) + if "complex128" in dtype_name: + return 16 + if "complex64" in dtype_name: + return 8 if "float32" in dtype_name or "int32" in dtype_name: return 4 if "float64" in dtype_name or "int64" in dtype_name: @@ -1008,3 +1025,204 @@ def gemm_fwd_roofline(op: "Op") -> tuple[int, int]: flops = 2 * m * n * k nbytes = (m * k + n * k + m * n) * elem_bytes return int(flops), int(nbytes) + + +def grouped_gemm_roofline(op: "Op") -> tuple[int, int]: + batch_sum = int(op.batch_sum) + batch_count = int(op.batch_count) + n = int(getattr(op, "N", getattr(op, "n", 0))) + k = int(getattr(op, "K", getattr(op, "k", 0))) + elem = _dtype_itemsize(getattr(op, "dtype", "float16")) + + flops = 2 * batch_sum * n * k + if not bool(op.transpose_a): + memory_a = batch_sum * k + memory_c = batch_sum * n + memory_b = batch_count * n * k + else: + memory_a = batch_sum * n + memory_c = batch_count * n * k + memory_b = k * batch_sum if bool(op.transpose_b) else batch_sum * k + return int(flops), int((memory_a + memory_b + memory_c) * elem) + + +def rope_roofline(op: "Op") -> tuple[int, int]: + seq_len = int(op.seq_len) + head_dim = int(op.head_dim) + batch = int(getattr(op, "batch", 1)) + num_heads = int(getattr(op, "num_heads", 1)) + layout = getattr(op, "layout", "1d") + elem = _dtype_itemsize(getattr(op, "dtype", "float16")) + outer = batch * num_heads if layout == "2d" else 1 + x_elems = outer * seq_len * head_dim + cos_sin_elems = seq_len * (head_dim // 2) * 2 + flops = 4 * x_elems + nbytes = (2 * x_elems + cos_sin_elems) * elem + return int(flops), int(nbytes) + + +def rope_position_ids_roofline(op: "Op") -> tuple[int, int]: + num_tokens = int(op.num_tokens) + num_heads = int(op.num_heads) + head_dim = int(op.head_dim) + rotary_dim = int(getattr(op, "rotary_dim", head_dim) or head_dim) + max_position = int(op.max_position) + elem = _dtype_itemsize(getattr(op, "dtype", "float16")) + x_elems = num_tokens * num_heads * head_dim + cos_sin_elems = max_position * (rotary_dim // 2) * 2 + pos_elems = num_tokens + return int(4 * x_elems), int((2 * x_elems + cos_sin_elems) * elem + pos_elems * 4) + + +def dropout_roofline(op: "Op") -> tuple[int, int]: + n_total = int(op.N_total) + elem = _dtype_itemsize(getattr(op, "dtype", "float16")) + if not bool(getattr(op, "training", True)) or float(getattr(op, "p", 0.5)) == 0.0: + return 0, int(2 * n_total * elem) + if float(getattr(op, "p", 0.5)) == 1.0: + return 0, int(n_total * elem) + return int(n_total), int(2 * n_total * elem) + + +def fp8_quant_roofline(op: "Op") -> tuple[int, int]: + batch = int(op.batch) + seq_len_kv = int(op.seq_len_kv) + kv_group = int(op.kv_group) + index_dim = int(op.index_dim) + in_elem = _dtype_itemsize(getattr(op, "in_dtype", "float16")) + groups = batch * seq_len_kv * kv_group + elems = groups * index_dim + flops = 6 * elems + groups + nbytes = elems * in_elem + elems * 1 + groups * 4 + return int(flops), int(nbytes) + + +def fp8_lighting_indexer_roofline(op: "Op") -> tuple[int, int]: + batch = int(op.batch) + seq_len = int(op.seq_len) + heads = int(op.heads) + index_dim = int(op.index_dim) + seq_len_kv = int(op.seq_len_kv) + kv_group = int(op.kv_group) + scores = batch * seq_len * seq_len_kv * kv_group + q_elems = batch * seq_len * heads * index_dim + k_elems = batch * seq_len_kv * kv_group * index_dim + weights = seq_len * heads + # The public forward accepts either bf16 tensors that are quantized inside + # the op or pre-quantized fp8 tensors. The op does not currently retain + # the observed input dtype, so default to bf16 for conservative bandwidth. + index_elem = _dtype_itemsize(getattr(op, "dtype", "bfloat16")) + flops = 2 * scores * index_dim + nbytes = (q_elems + k_elems) * index_elem + batch * seq_len_kv * kv_group * 4 + nbytes += weights * 4 + nbytes += 2 * seq_len * 4 + scores * 4 + return int(flops), int(nbytes) + + +def topk_selector_roofline(op: "Op") -> tuple[int, int]: + batch = int(op.batch) + seq_len = int(op.seq_len) + seq_len_kv = int(op.seq_len_kv) + kv_group = int(op.kv_group) + topk = int(op.topk) + in_elem = _dtype_itemsize(getattr(op, "in_dtype", "float32")) + out_elem = _dtype_itemsize(getattr(op, "out_dtype", "int32")) + comparisons = batch * seq_len * kv_group * seq_len_kv + nbytes = comparisons * in_elem + batch * seq_len * 2 * out_elem + nbytes += batch * seq_len * kv_group * topk * out_elem + return int(comparisons), int(nbytes) + + +def _engram_elem_bytes(op: "Op") -> int: + return _dtype_itemsize(getattr(op, "dtype", "float16")) + + +def engram_gate_conv_fwd_roofline(op: "Op") -> tuple[int, int]: + m = int(op.M) + seq_len = int(op.seq_len) + d = int(getattr(op, "d_padded", op.d)) + elem = _engram_elem_bytes(op) + flops = m * seq_len * (24 * d) + 20 * m * seq_len + nbytes = (5 * m * seq_len * d) * elem + 4 * m * seq_len * 4 + 6 * d * elem + return int(flops), int(nbytes) + + +def engram_gate_conv_bwd_roofline(op: "Op") -> tuple[int, int]: + m = int(op.M) + seq_len = int(op.seq_len) + d = int(getattr(op, "d_padded", op.d)) + elem = _engram_elem_bytes(op) + fwd_flops = m * seq_len * (24 * d) + 20 * m * seq_len + read_bytes = 5 * m * seq_len * d * elem + 6 * d * elem + 4 * m * seq_len * 4 + write_bytes = 3 * m * seq_len * d * elem + 10 * d * 4 + m * seq_len * d * 4 + return int(fwd_flops * 2.5), int(read_bytes + write_bytes) + + +def engram_decode_roofline(op: "Op") -> tuple[int, int]: + batch = int(op.batch) + d_mem = int(op.d_mem) + d = int(getattr(op, "d_padded", op.d)) + max_conv_len = int(op.max_conv_len) + conv_kernel_size = int(op.conv_kernel_size) + elem = _engram_elem_bytes(op) + flops = ( + 4 * batch * d_mem * d + + batch * (16 * d + conv_kernel_size * 2 * d) + + 20 * batch + ) + nbytes = ( + batch * d_mem + + batch * d + + 2 * batch * max_conv_len * d + + 2 * d_mem * d + + 2 * d + + conv_kernel_size * d + + batch * d + ) * elem + return int(flops), int(nbytes) + + +def fft_c2c_roofline(op: "Op") -> tuple[int, int]: + import math + + n = int(op.n) + elem = _dtype_itemsize(getattr(op, "dtype", "complex64")) + # Runtime batch is inferred from the input and kernel cache. Before a + # forward call, the constructed default kernel represents batch=1. + batch = int(getattr(getattr(op, "kernel", None), "batch_size", 1) or 1) + return int(batch * 5 * n * math.log2(n)), int(batch * 2 * n * elem) + + +def mhc_pre_roofline(op: "Op") -> tuple[int, int]: + batch = int(op.batch) + n_expand = int(op.n_expand) + c_x = int(op.c_x) + x_dim = n_expand * c_x + phi_dim = n_expand * n_expand + 2 * n_expand + x_elem = _dtype_itemsize(getattr(op, "dtype", "bfloat16")) + + x_phi_flops = 2 * batch * x_dim * phi_dim + x_layer_flops = 2 * batch * c_x * n_expand + x_res_flops = 2 * batch * n_expand * c_x * n_expand + flops = x_phi_flops + x_layer_flops + x_res_flops + + phi_bytes = x_dim * phi_dim * 4 + b_bytes = phi_dim * 4 + x_bytes = batch * x_dim * x_elem + output_bytes = batch * (x_dim + c_x) * x_elem + nbytes = phi_bytes + b_bytes + x_bytes + output_bytes + return int(flops), int(nbytes) + + +def mhc_post_roofline(op: "Op") -> tuple[int, int]: + batch = int(op.batch) + n_expand = int(op.n_expand) + c_x = int(op.c_x) + x_elem = _dtype_itemsize(getattr(op, "dtype", "bfloat16")) + flops = 2 * batch * n_expand * c_x + x_layer_out_bytes = batch * c_x * x_elem + h_post_bytes = batch * n_expand * 4 + x_res_bytes = batch * n_expand * c_x * x_elem + x_out_bytes = batch * n_expand * c_x * x_elem + nbytes = x_layer_out_bytes + h_post_bytes + x_res_bytes + x_out_bytes + return int(flops), int(nbytes)