84 lines
3.7 KiB
Bash
Executable file
84 lines
3.7 KiB
Bash
Executable file
#!/bin/sh
|
|
# Derive minimum logical traffic for Gemma 4 31B split-score decode attention.
|
|
set -eu
|
|
|
|
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
|
|
echo "usage: $0 PROFILE.json [OUTPUT.json]" >&2
|
|
exit 2
|
|
fi
|
|
profile=$1
|
|
output=${2:-}
|
|
[ -f "$profile" ] || { echo "gem: decode profile is missing: $profile" >&2; exit 2; }
|
|
if [ -n "$output" ] && { [ -e "$output" ] || [ -L "$output" ]; }; then
|
|
echo "gem: traffic analysis output already exists: $output" >&2
|
|
exit 2
|
|
fi
|
|
|
|
jq -e '
|
|
.schema_version == 1 and .engine == "gem-native-c-metal" and
|
|
.workload == "synthetic-exact-shape-decode-attention-stage-profile" and
|
|
.shape == "31b-full" and .query_heads == 32 and .kv_heads == 4 and
|
|
.head_dimension == 512 and .window == 0 and
|
|
(.live_tokens | type) == "number" and .live_tokens >= 8192 and
|
|
.live_tokens == (.live_tokens | floor) and
|
|
(.value_dimension_tile | type) == "number" and .value_dimension_tile > 0 and
|
|
.value_dimension_tile == (.value_dimension_tile | floor) and
|
|
(.head_dimension % .value_dimension_tile) == 0 and
|
|
(.score_scratch_bytes == .query_heads * .live_tokens * 4) and
|
|
((.value_partitions // 1) | type) == "number" and
|
|
(.value_partitions // 1) >= 1 and
|
|
(.value_partitions // 1) == ((.value_partitions // 1) | floor) and
|
|
(if (.value_partitions // 1) > 1 then
|
|
.value_partition_scratch_bytes ==
|
|
.query_heads * .head_dimension * (.value_partitions // 1) * 4
|
|
else true end)
|
|
' "$profile" >/dev/null || {
|
|
echo "gem: profile does not satisfy the 31B F32 decode traffic contract" >&2
|
|
exit 1
|
|
}
|
|
|
|
profile_hash=$(shasum -a 256 "$profile" | awk '{print $1}')
|
|
temporary=$(mktemp /private/tmp/gem-decode-traffic.XXXXXX)
|
|
trap 'rm -f "$temporary"' EXIT INT TERM HUP
|
|
jq --arg source "$profile" --arg profile_hash "$profile_hash" '
|
|
(.query_heads * .live_tokens * 4) as $score |
|
|
(.kv_heads * .head_dimension * .live_tokens * 4) as $cache_side |
|
|
(.head_dimension / .value_dimension_tile) as $dimension_groups |
|
|
(.value_partitions // 1) as $partitions |
|
|
(if $partitions > 1 then .value_partition_scratch_bytes else 0 end) as $partials |
|
|
(.query_heads * .head_dimension * 4) as $output |
|
|
($score * (1 + 5 + $dimension_groups)) as $score_intermediate |
|
|
($cache_side * 2 + $score_intermediate + $partials * 2 + $output) as $total |
|
|
{schema_version: 1, engine: "gem-native-c-metal",
|
|
workload: "decode-attention-minimum-logical-traffic",
|
|
source_profile: $source, source_profile_sha256: $profile_hash,
|
|
shape: {name: .shape, query_heads, kv_heads, head_dimension,
|
|
live_tokens, value_dimension_tile,
|
|
value_dimension_groups: $dimension_groups,
|
|
value_partitions: $partitions},
|
|
bytes: {key_cache_read: $cache_side, value_cache_read: $cache_side,
|
|
raw_score_write: $score,
|
|
normalize_score_io: (5 * $score),
|
|
value_score_read: ($dimension_groups * $score),
|
|
avoidable_score_intermediate: $score_intermediate,
|
|
value_partial_write: $partials, value_partial_reduce_read: $partials,
|
|
output_write: $output, modeled_total: $total},
|
|
ratios: {
|
|
score_intermediate_over_kv_cache_read_pct:
|
|
(100 * $score_intermediate / (2 * $cache_side)),
|
|
bounded_partial_over_kv_cache_read_pct:
|
|
(100 * (2 * $partials + $output) / (2 * $cache_side))},
|
|
interpretation: {
|
|
accounting: "minimum-logical-bytes-not-device-counter-traffic",
|
|
normalization_passes: ["max-read", "exp-read-write", "scale-read-write"],
|
|
value_score_reuse: "one full score-row read per output-dimension group",
|
|
promotion_eligible: false,
|
|
required_next_evidence: "Metal bandwidth/cache counters and authentic whole-token A/B"}}
|
|
' "$profile" > "$temporary"
|
|
|
|
if [ -n "$output" ]; then
|
|
mv "$temporary" "$output"
|
|
trap - EXIT INT TERM HUP
|
|
else
|
|
cat "$temporary"
|
|
fi
|