Skip to main content

hrx/loom/report/
guidance.rs

1use super::CompileReport;
2use crate::{Error, Result};
3use serde::Serialize;
4use serde_json::Value;
5use std::collections::{BTreeMap, BTreeSet};
6
7/// Experiments suggested by retained compiler evidence, not measured speedups.
8#[derive(Clone, Debug, Serialize)]
9pub struct ReportGuidance {
10    /// Whether source-memory bank groups were captured.
11    pub bank_service_available: bool,
12    /// Whether per-resource residency constraints were captured.
13    pub residency_available: bool,
14    /// Suggestions supported by complete, qualified evidence.
15    pub suggestions: Vec<ReportSuggestion>,
16}
17
18/// One compiler-supported experiment and the exact facts supporting it.
19#[derive(Clone, Debug, Serialize)]
20pub struct ReportSuggestion {
21    /// Stable suggestion category.
22    pub kind: &'static str,
23    /// Compiled entry to which the experiment applies.
24    pub function: String,
25    /// Suggested experiment, including model limitations.
26    pub action: String,
27    /// JSON pointers into [`CompileReport::json`] and their original values.
28    pub evidence: BTreeMap<String, Value>,
29}
30
31impl CompileReport {
32    /// Suggest residency and LDS layout experiments using producer-supplied
33    /// thresholds. Unavailable evidence and unvalidated bank models yield no
34    /// recommendations. All joint residency limits must be captured.
35    pub fn guidance(&self) -> Result<ReportGuidance> {
36        self.validate()?;
37        let constraints = self.rows("residency_constraints")?;
38        let groups = self
39            .document
40            .pointer("/source_low/memory/bank_service_groups");
41        let mut guidance = ReportGuidance {
42            bank_service_available: groups.is_some_and(|v| !v.is_null()),
43            residency_available: constraints.is_some(),
44            suggestions: Vec::new(),
45        };
46        if self
47            .document
48            .pointer("/status/code")
49            .and_then(Value::as_u64)
50            != Some(0)
51            || self.document["target_family"] != "amdgpu"
52        {
53            return Ok(guidance);
54        }
55        let entries = self.rows("entries")?.unwrap_or_default();
56        let mut names = BTreeSet::new();
57        for entry in entries {
58            if let Some(name) = entry["function"].as_str()
59                && (!names.insert(name) || name.is_empty())
60            {
61                return Err(Error::Message(
62                    "report has ambiguous entry identities".into(),
63                ));
64            }
65        }
66        if let Some(constraints) = constraints {
67            for (index, entry) in entries.iter().enumerate() {
68                let Some(function) = entry["function"].as_str() else {
69                    continue;
70                };
71                let Some(summary) = entry.pointer("/target_resources/residency") else {
72                    continue;
73                };
74                let (Some(current), Some(next), Some(limiters)) = (
75                    summary["current_tier"].as_u64(),
76                    summary["next_better_tier"].as_u64(),
77                    summary["limiting_resource_count"].as_u64(),
78                ) else {
79                    continue;
80                };
81                if next <= current || limiters == 0 {
82                    continue;
83                }
84                let limiting: Vec<_> = constraints
85                    .iter()
86                    .enumerate()
87                    .filter(|(_, row)| row["function"] == function && row["limiting"] == true)
88                    .collect();
89                if limiting.len() as u64 != limiters {
90                    continue;
91                }
92                let mut reductions = Vec::new();
93                let mut resources = BTreeSet::new();
94                let mut evidence = BTreeMap::new();
95                for (position, row) in limiting {
96                    let (Some(name), Some(unit), Some(scope), Some(units), Some(reduction)) = (
97                        row["name"].as_str(),
98                        row["unit"].as_str(),
99                        row["allocation_scope"].as_str(),
100                        row["units"].as_u64(),
101                        row["reduction_units_to_next_better_tier"].as_u64(),
102                    ) else {
103                        break;
104                    };
105                    let Some(maximum) = units.checked_sub(reduction) else {
106                        return Err(Error::Message(
107                            "residency reduction exceeds resource usage".into(),
108                        ));
109                    };
110                    if !resources.insert(name) || name.is_empty() || reduction == 0 {
111                        return Err(Error::Message(
112                            "report has invalid residency requirements".into(),
113                        ));
114                    }
115                    reductions.push(format!(
116                        "{name} by {reduction} {unit}/{scope} to at most {maximum}"
117                    ));
118                    cite(
119                        &mut evidence,
120                        &format!("/residency_constraints/rows/{position}"),
121                        row,
122                    );
123                }
124                if reductions.len() as u64 != limiters {
125                    continue;
126                }
127                cite(
128                    &mut evidence,
129                    &format!("/entries/rows/{index}/target_resources/residency"),
130                    summary,
131                );
132                guidance.suggestions.push(ReportSuggestion {
133                    kind: "amdgpu.residency_cliff", function: function.into(), evidence,
134                    action: format!("Reduce {} together to target modeled residency {current} -> {next} subgroups/SIMD. Recompile and benchmark; higher residency does not guarantee higher throughput.", reductions.join(" and ")),
135                });
136            }
137        }
138        if guidance.bank_service_available {
139            let groups = groups
140                .and_then(Value::as_array)
141                .ok_or_else(|| Error::Message("report bank groups must be an array".into()))?;
142            if self
143                .document
144                .pointer("/source_low/memory/bank_service_group_count")
145                .and_then(Value::as_u64)
146                != Some(groups.len() as u64)
147            {
148                return Err(Error::Message(
149                    "report bank group count does not match its rows".into(),
150                ));
151            }
152            for (index, group) in groups.iter().enumerate() {
153                let Some(function) = group["function"]
154                    .as_str()
155                    .filter(|name| names.contains(name))
156                else {
157                    continue;
158                };
159                if !matches!(
160                    group.pointer("/model/evidence").and_then(Value::as_str),
161                    Some("public-vendor-documentation" | "silicon-calibrated-vendor-model")
162                ) {
163                    continue;
164                }
165                let summary = &group["summary"];
166                let (Some(exact), Some(conflicts), Some(extra), Some(required), Some(uncontended)) = (
167                    summary["exact_packet_count"].as_u64(),
168                    summary
169                        .pointer("/structural/conflicted_packet_count")
170                        .and_then(Value::as_u64),
171                    summary
172                        .pointer("/structural/extra_round_count")
173                        .and_then(Value::as_u64),
174                    summary
175                        .pointer("/structural/required_round_count")
176                        .and_then(Value::as_u64),
177                    summary
178                        .pointer("/structural/uncontended_round_count")
179                        .and_then(Value::as_u64),
180                ) else {
181                    continue;
182                };
183                if exact == 0 || conflicts == 0 || extra == 0 {
184                    continue;
185                }
186                if conflicts > exact
187                    || uncontended == 0
188                    || uncontended.checked_add(extra) != Some(required)
189                {
190                    return Err(Error::Message(
191                        "report bank service counts are inconsistent".into(),
192                    ));
193                }
194                let mut evidence = BTreeMap::new();
195                cite(
196                    &mut evidence,
197                    &format!("/source_low/memory/bank_service_groups/{index}"),
198                    group,
199                );
200                let location = group["source_root"].as_str().unwrap_or("this access group");
201                guidance.suggestions.push(ReportSuggestion {
202                    kind: "amdgpu.lds_bank_service", function: function.into(), evidence,
203                    action: format!("Test a pitch or padding change for {location}: {conflicts} proven packet sites require {extra} extra bank-service rounds. These are structural counts, not measured cycles; unknown and unmodeled accesses remain unqualified. Check the resulting LDS residency and benchmark before adopting the layout."),
204                });
205            }
206        }
207        Ok(guidance)
208    }
209}
210
211fn cite(evidence: &mut BTreeMap<String, Value>, pointer: &str, value: &Value) {
212    evidence.insert(pointer.into(), value.clone());
213}