diff --git a/custom_dropdown.py b/custom_dropdown.py new file mode 100644 index 0000000..60a4ab2 --- /dev/null +++ b/custom_dropdown.py @@ -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""" + + """ + 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 diff --git a/dashboard.py b/dashboard.py index 7445434..93dac2b 100644 --- a/dashboard.py +++ b/dashboard.py @@ -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']}_") + 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 diff --git a/dashboard_styles.py b/dashboard_styles.py index 2bb618c..5db54d7 100644 --- a/dashboard_styles.py +++ b/dashboard_styles.py @@ -1001,6 +1001,22 @@ def get_app_css(): color: #ffffff !important; } + /* ℹ️ info icon — only on ISL/OSL selectboxes (class added via JS) */ + .profile-info-icon [data-testid="stTooltipHoverTarget"] { + position: relative !important; + } + .profile-info-icon [data-testid="stTooltipHoverTarget"] svg { + opacity: 0 !important; + } + .profile-info-icon [data-testid="stTooltipHoverTarget"]::after { + content: "ℹ️" !important; + position: absolute !important; + top: 0 !important; + left: 0 !important; + font-size: 0.85rem !important; + pointer-events: none !important; + } + """ diff --git a/llmd_dashboard.py b/llmd_dashboard.py index 5e3db9d..795adae 100644 --- a/llmd_dashboard.py +++ b/llmd_dashboard.py @@ -18,6 +18,8 @@ import plotly.io as pio import streamlit as st +from custom_dropdown import get_profile_details, inject_profile_tooltips + # Set global Plotly template if not already set by main dashboard if "plotly_white_light" not in pio.templates: _light_hover = go.layout.Template( @@ -907,16 +909,36 @@ def render_llmd_filters(df: pd.DataFrame) -> tuple[pd.DataFrame, dict]: else profiles[0] ) - selected_profile = ( - st.selectbox( + if profiles: + selected_profile = st.selectbox( "2️⃣ Select Input/Output Sequence Length (ISL/OSL)", - profiles, - format_func=clean_profile_name, + options=profiles, key=profile_key, + help="Hover over dropdown options to see guidellm profile details", ) - if profiles - else None - ) + else: + selected_profile = None + + 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']}_") + st.caption( "Please refer to the notes column in the filtered data to understand more about the workload profile." ) @@ -5419,6 +5441,8 @@ def render_llmd_dashboard(llmd_csv_path: str): Args: llmd_csv_path: Path to the LLM-D CSV data file """ + inject_profile_tooltips() + # Load data df = load_llmd_data(llmd_csv_path)