Repository navigation
Add guidellm profile tooltip to ISL/OSL dropdown #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,239 @@ | ||
| """Inject hover tooltips onto Streamlit selectbox options for profile dropdowns.""" | ||
|
|
||
| import json | ||
|
|
||
| import streamlit.components.v1 as components | ||
|
|
||
| PROFILE_DETAILS = { | ||
| "1000/1000": { | ||
| "name": "Balanced Profile", | ||
| "prompt_tokens": "1000", | ||
| "output_tokens": "1000", | ||
| }, | ||
| "512/2048": { | ||
| "name": "Heterogeneous", | ||
| "prompt_tokens": "512 (stdev=128, min=1, max=1024)", | ||
| "output_tokens": "2048 (stdev=512, min=1, max=4096)", | ||
| }, | ||
| "2048/128": { | ||
| "name": "Short Prefill-Heavy", | ||
| "prompt_tokens": "2048", | ||
| "output_tokens": "128", | ||
| }, | ||
| "8000/1000": { | ||
| "name": "Prefill-Heavy", | ||
| "prompt_tokens": "8000", | ||
| "output_tokens": "1000", | ||
| "samples": "50", | ||
| }, | ||
| "100000/1000": { | ||
| "name": "Long Context Prefill-Heavy", | ||
| "prompt_tokens": "100000", | ||
| "output_tokens": "1000", | ||
| "samples": "10", | ||
| }, | ||
| "8000/800": { | ||
| "name": "Heavy Heterogeneous", | ||
| "prompt_tokens": "8000 (stdev=8500, min=50, max=30000)", | ||
| "output_tokens": "800 (stdev=1500, min=20, max=8000)", | ||
| "description": "Simulates realistic chat patterns with large prompts and smaller outputs. Samples 450 seconds of traffic.", | ||
| }, | ||
| "128/128": { | ||
| "name": "Multi-turn Profile", | ||
| "prompt_tokens": "128", | ||
| "output_tokens": "128", | ||
| "turns": "5", | ||
| "prefix_tokens": "512", | ||
| "prefix_count": "10,000", | ||
| "description": "Multi-turn conversation benchmark with 5 turns and 512-token prefix.", | ||
| "aliases": ["Multi-turn"], | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| def get_profile_details(profile_name: str) -> dict: | ||
| """Get profile details by name, normalizing k-notation (e.g. 1k/1k -> 1000/1000).""" | ||
| if profile_name in PROFILE_DETAILS: | ||
| return PROFILE_DETAILS[profile_name] | ||
|
|
||
| if not profile_name: | ||
| return {} | ||
|
|
||
| if "(" in profile_name and ")" in profile_name: | ||
| start = profile_name.rfind("(") + 1 | ||
| end = profile_name.rfind(")") | ||
| token_pair = profile_name[start:end].strip() | ||
| else: | ||
| token_pair = profile_name | ||
|
|
||
| parts = token_pair.split("/") | ||
| normalized = [] | ||
| for part in parts: | ||
| if "k" in part.lower(): | ||
| num = float(part.lower().replace("k", "")) * 1000 | ||
| normalized.append(str(int(num))) | ||
| else: | ||
| normalized.append(part) | ||
|
|
||
| key = "/".join(normalized) | ||
| return PROFILE_DETAILS.get(key, {}) | ||
|
|
||
|
|
||
| def inject_profile_tooltips() -> None: | ||
| """Inject JS that adds hover tooltips to all ISL/OSL profile selectbox options. | ||
|
|
||
| Uses st.components.v1.html() to run JavaScript that accesses the parent | ||
| Streamlit document via window.parent.document. A MutationObserver watches | ||
| for [role="option"] elements and attaches tooltip behavior on hover. | ||
| """ | ||
| tooltip_map = {} | ||
| for key, details in PROFILE_DETAILS.items(): | ||
| lines = [details["name"]] | ||
| lines.append(f"Input: {details['prompt_tokens']}") | ||
| lines.append(f"Output: {details['output_tokens']}") | ||
| if details.get("samples"): | ||
| lines.append(f"Samples: {details['samples']}") | ||
| if details.get("turns"): | ||
| lines.append(f"Turns: {details['turns']}") | ||
| if details.get("prefix_tokens"): | ||
| lines.append(f"Prefix Token: {details['prefix_tokens']}") | ||
| if details.get("prefix_count"): | ||
| lines.append(f"Prefix Count: {details['prefix_count']}") | ||
| if details.get("description"): | ||
| lines.append(details["description"]) | ||
| tooltip_map[key] = "\n".join(lines) | ||
|
|
||
| for key in list(tooltip_map): | ||
| for variant in _generate_display_variants(key): | ||
| if variant not in tooltip_map: | ||
| tooltip_map[variant] = tooltip_map[key] | ||
|
|
||
| js_code = f""" | ||
| <script> | ||
| (function() {{ | ||
| const doc = window.parent.document; | ||
| if (doc.__profileTooltipInjected) return; | ||
| doc.__profileTooltipInjected = true; | ||
|
|
||
| const tooltips = {json.dumps(tooltip_map)}; | ||
|
|
||
| const extractKey = (text) => {{ | ||
| text = text.trim(); | ||
| if (tooltips[text]) return text; | ||
| const m = text.match(/\\(([^)]+)\\)\\s*$/); | ||
| if (m) {{ | ||
| let pair = m[1]; | ||
| pair = pair.replace(/(\\d+)k/gi, (_, n) => String(Number(n) * 1000)); | ||
| if (tooltips[pair]) return pair; | ||
| }} | ||
| let norm = text.replace(/(\\d+)k/gi, (_, n) => String(Number(n) * 1000)); | ||
| if (tooltips[norm]) return norm; | ||
| return null; | ||
| }}; | ||
|
|
||
| const tip = doc.createElement('div'); | ||
| tip.id = 'profile-tooltip'; | ||
| tip.style.cssText = ` | ||
| position: fixed; background: #ffffff; color: #1a1a1a; | ||
| padding: 10px 14px; border-radius: 6px; font-size: 0.8rem; | ||
| font-family: "Source Sans Pro", sans-serif; | ||
| max-width: 340px; z-index: 100001; pointer-events: none; | ||
| opacity: 0; transition: opacity 0.15s; white-space: pre-line; | ||
| line-height: 1.5; box-shadow: 0 4px 12px rgba(0,0,0,0.2); | ||
| border: 1px solid #e0e0e0; | ||
| `; | ||
| doc.body.appendChild(tip); | ||
|
|
||
| const show = (el, key) => {{ | ||
| tip.textContent = tooltips[key]; | ||
| const rect = el.getBoundingClientRect(); | ||
| let left = rect.right + 12; | ||
| if (left + 350 > doc.documentElement.clientWidth) {{ | ||
| left = rect.left - 350 - 12; | ||
| if (left < 8) left = 8; | ||
| }} | ||
| tip.style.left = left + 'px'; | ||
| tip.style.top = Math.max(8, rect.top) + 'px'; | ||
| tip.style.opacity = '1'; | ||
| }}; | ||
|
|
||
| const hide = () => {{ tip.style.opacity = '0'; }}; | ||
|
|
||
| const bind = (opt) => {{ | ||
| if (opt.dataset.ttBound) return; | ||
| const key = extractKey(opt.textContent); | ||
| if (!key) return; | ||
| opt.dataset.ttBound = '1'; | ||
| opt.addEventListener('mouseenter', () => show(opt, key)); | ||
| opt.addEventListener('mouseleave', hide); | ||
| }}; | ||
|
|
||
| new MutationObserver(() => {{ | ||
| doc.querySelectorAll('[role="option"]').forEach(bind); | ||
|
|
||
| // Tag ISL/OSL tooltip icons with profile-info-icon class | ||
| doc.querySelectorAll('[data-testid="stWidgetLabel"]').forEach(label => {{ | ||
| const text = label.textContent || ''; | ||
| if (text.includes('ISL') || text.includes('OSL') || text.includes('Sequence Length')) {{ | ||
| const container = label.closest('[data-testid="stSelectbox"], [data-testid="stMultiSelect"]'); | ||
| if (!container) return; | ||
| const icon = container.querySelector('.stTooltipIcon'); | ||
| if (icon && !icon.classList.contains('profile-info-icon')) {{ | ||
| icon.classList.add('profile-info-icon'); | ||
| }} | ||
| }} | ||
| }}); | ||
| }}).observe(doc.body, {{ childList: true, subtree: true }}); | ||
| }})(); | ||
| </script> | ||
| """ | ||
| components.html(js_code, height=0) | ||
|
|
||
|
|
||
| def _generate_display_variants(token_pair: str) -> list[str]: | ||
| """Generate display name variants for a token pair like '1000/1000'. | ||
|
|
||
| Auto-generates k-notation variants (e.g. 1000/1000 -> 1k/1k, (1k/1k)). | ||
| Additional aliases (e.g. 'Multi-turn') come from the 'aliases' field in PROFILE_DETAILS. | ||
| """ | ||
| variants = [] | ||
| parts = token_pair.split("/") | ||
| if len(parts) != 2: | ||
| return variants | ||
|
|
||
| def to_k(val: str) -> str: | ||
| try: | ||
| n = int(val) | ||
| if n >= 1000 and n % 1000 == 0: | ||
| return f"{n // 1000}k" | ||
| except ValueError: | ||
| pass | ||
| return val | ||
|
|
||
| k_input = to_k(parts[0]) | ||
| k_output = to_k(parts[1]) | ||
| k_pair = f"{k_input}/{k_output}" | ||
| if k_pair != token_pair: | ||
| variants.append(k_pair) | ||
| variants.append(f"({k_pair})") | ||
|
|
||
| # The dropdown may show rounded k-notation (e.g. 2048 as "2k") which the JS | ||
| # decodes as 2000. Add that decoded form so the lookup still hits. | ||
| def k_decoded(val: str) -> str: | ||
| try: | ||
| n = int(val) | ||
| if n >= 1000: | ||
| return str((n // 1000) * 1000) | ||
| except ValueError: | ||
| pass | ||
| return val | ||
|
|
||
| decoded = f"{k_decoded(parts[0])}/{k_decoded(parts[1])}" | ||
| if decoded != token_pair and decoded not in variants: | ||
| variants.append(decoded) | ||
|
|
||
| for alias in PROFILE_DETAILS.get(token_pair, {}).get("aliases", []): | ||
| if alias not in variants: | ||
| variants.append(alias) | ||
|
|
||
| return variants | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ | |
| import streamlit.components.v1 as _stc | ||
| from plotly.subplots import make_subplots | ||
|
|
||
| from custom_dropdown import get_profile_details, inject_profile_tooltips | ||
| from dashboard_styles import ( | ||
| apply_theme_css, | ||
| get_app_css, | ||
|
|
@@ -337,6 +338,16 @@ def load_data(file_path, cache_key=None): | |
| df["model"] = df["model"].str.strip() | ||
| df["version"] = df["version"].str.strip() | ||
| df["TP"] = pd.to_numeric(df["TP"], errors="coerce") | ||
|
|
||
| # Filter out rows with missing critical fields - these indicate data quality issues | ||
| initial_row_count = len(df) | ||
| df = df.dropna(subset=["accelerator", "model", "version"]) | ||
| dropped_rows = initial_row_count - len(df) | ||
| if dropped_rows > 0: | ||
| logger.warning( | ||
| f"Dropped {dropped_rows} rows with missing accelerator/model/version" | ||
| ) | ||
|
|
||
| return df | ||
| except FileNotFoundError: | ||
| st.error( | ||
|
|
@@ -5217,8 +5228,29 @@ def _best_default_model(model_list: list[str]) -> str: | |
| key=profile_key, | ||
| on_change=keep_expander_open, | ||
| args=("performance_trends_expanded",), | ||
| help="Hover over dropdown options to see guidellm profile details", | ||
| ) | ||
|
|
||
| if selected_profile: | ||
| details = get_profile_details(selected_profile) | ||
| if details: | ||
| with st.container(border=True): | ||
| st.markdown(f"**{details['name']}**") | ||
| col1, col2 = st.columns(2) | ||
| with col1: | ||
| st.write(f"**Input tokens:** {details['prompt_tokens']}") | ||
| with col2: | ||
| st.write(f"**Output tokens:** {details['output_tokens']}") | ||
|
|
||
| if details.get("samples"): | ||
| st.write(f"**Samples:** {details['samples']}") | ||
| if details.get("turns"): | ||
| st.write(f"**Conversation turns:** {details['turns']}") | ||
| if details.get("prefix_tokens"): | ||
| st.write(f"**Prefix tokens:** {details['prefix_tokens']}") | ||
|
|
||
| st.write(f"_{details['description']}_") | ||
|
|
||
|
Comment on lines
+5245
to
+5253
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
This section's own default profile is Line 5252 accesses 🐛 Proposed fix if details.get("prefix_tokens"):
st.write(f"**Prefix tokens:** {details['prefix_tokens']}")
- st.write(f"_{details['description']}_")
+ if details.get("description"):
+ st.write(f"_{details['description']}_")The identical pattern also exists in 🤖 Prompt for AI Agents |
||
| profile_df = model_df[model_df["profile"] == selected_profile].copy() | ||
|
|
||
| with filter_col5: | ||
|
|
@@ -5855,6 +5887,7 @@ def render_compare_versions_summary_section(df, use_expander=True): | |
| key="compare_summary_profile", | ||
| on_change=keep_expander_open, | ||
| args=("compare_versions_summary_expanded",), | ||
| help="Hover over dropdown options to see guidellm profile details", | ||
| ) | ||
|
|
||
| # Secondary custom ISL/OSL pair filter | ||
|
|
@@ -10655,6 +10688,10 @@ def convert_epoch_to_date(row): | |
| "dashboard_id": "psap-b200-mlperf", | ||
| "dashboard_name": "vllm-2b-dcgm-metrics-psap-b200-mlperf", | ||
| }, | ||
| "B200_PIRATE": { | ||
| "dashboard_id": "b200-pirate-vllm-dcgm", | ||
| "dashboard_name": "vllm-2b-dcgm-metrics-b200-pirate", | ||
| }, | ||
| } | ||
|
|
||
| # Jan 1, 2026 00:00:00 UTC in milliseconds | ||
|
|
@@ -11112,6 +11149,7 @@ def render_confidentiality_notice(): | |
|
|
||
| st.markdown(get_app_css(), unsafe_allow_html=True) | ||
| apply_theme_css() | ||
| inject_profile_tooltips() | ||
|
|
||
| render_sidebar_header() | ||
|
|
||
|
|
@@ -11406,7 +11444,7 @@ def decode_filters_from_url(): | |
| all_accelerators = sorted(df["accelerator"].unique().tolist()) | ||
| all_models = sorted(df["model"].unique().tolist()) | ||
| all_versions = sorted(df["version"].unique().tolist()) | ||
| all_profiles = sorted(df["profile"].unique().tolist()) | ||
| all_profiles = sorted(df["profile"].dropna().astype(str).unique().tolist()) | ||
| all_tp_sizes = sorted(df["TP"].dropna().unique().tolist()) | ||
|
|
||
| url_accelerators = [] | ||
|
|
@@ -12041,6 +12079,7 @@ def decode_filters_from_url(): | |
| index=_profile_idx, | ||
| format_func=clean_profile_name, | ||
| key=profile_key, | ||
| help="Hover over dropdown options to see guidellm profile details", | ||
| ) | ||
| if profiles | ||
| else None | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: openshift-psap/performance-dashboard
Length of output: 9531
🏁 Script executed:
Repository: openshift-psap/performance-dashboard
Length of output: 9872
🏁 Script executed:
Repository: openshift-psap/performance-dashboard
Length of output: 10195
Handle rounded k-notation and profile aliases in
get_profile_details.get_profile_detailsconverts512/2kto512/2000and2k/128to2000/128, butPROFILE_DETAILScontains512/2048and2048/128. The details panel is therefore empty for those dashboard selections. The function also returns{}when a caller passes the registeredMulti-turnalias.🐛 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents