Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/d3-charts.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,11 @@ Label loops that size a background rect to its text (`.ll-bg`/`.pl-bg`, point-la
Every `setAttribute`, inline-style write, `textContent` assignment, and node move produces a `MutationObserver` record, **even when the value is unchanged**. PostHog session replay (rrweb) serializes each record — a moved node is re-serialized with its whole subtree — and the agentic-point coach mark rescans on them. A throttled Chrome profile of a legend toggle attributed ~40% of the main-thread work to rrweb processing these records; most came from blind re-syncs of hundreds of point labels, decoration rings, gradient stops, and `.raise()` calls that were already in order.

Hot re-sync paths (data, metric, and display phases; zoom frames; point-label placement) therefore use the write-if-changed helpers in `src/lib/d3-chart/chart-update.ts`: `setAttrIfChanged`, `setStyleIfChanged`, `setTextIfChanged`, and `raiseInOrder` (sequential `.raise()` semantics, skipped when each parent's tail is already in that order). They compare against the live DOM, not a cache, so a label the placement pass moved is still restored correctly. Reads of attributes and inline styles never force layout. Initial enter-path construction can keep plain `.attr()` chains — new nodes produce one add record regardless. Tests for these paths assert a repeated pass emits zero mutation records.

## Huawei series identity

Huawei uses a shared amber/gold identity, separate from NVIDIA green, AMD red, Google blue and OpenAI purple. `HUAWEI_CHART_PALETTE` (including the `HUAWEI_CHART_COLORS` tokens) in `packages/constants/src/gpu-keys.ts` defines a darker amber for light canvases and a brighter gold for dark canvases. Both base colors exceed 4.5:1 contrast against the tested white and `#111214` reference backgrounds.

`getVendor` recognizes canonical Ascend keys, framework-suffixed keys and Huawei/Ascend display names. The shared normal, date and high-contrast generators retain this identity; multiple Huawei chips/configurations use related amber shades. High-contrast palettes reserve the amber band from other vendors when Huawei is present. OperatorX delegates to the shared generator instead of maintaining a local crimson override. ubenchX uses the same generator for Huawei marks, legend swatches and tooltips while retaining its existing GPU palette. Theme changes update display decoration without changing measurement coordinates.

These colors encode vendor identity, not a measurement. Sequential heatmap scales encode values and are independent of categorical vendor colors; official logo artwork is also separate. This is presentation-only: no API values, filters, hardware specifications or normalization references change.
73 changes: 73 additions & 0 deletions packages/app/cypress/e2e/operatorx.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,76 @@ describe('OperatorX progressive rendering', { testIsolation: true }, () => {
.should('exist');
});
});

const operatorPoints = () =>
cy
.get('[data-testid="operatorx-viz-roofline"] circle.point')
.filter(
(_, element) =>
(element as unknown as { __data__: { hw: string } }).__data__.hw === 'ascend950pr',
);

const bandwidthPoints = () =>
cy
.get('#ubenchx-mbu circle.point')
.filter(
(_, element) =>
(element as unknown as { __data__: { gpuKey: string } }).__data__.gpuKey === 'Huawei 950PR',
);

it('uses shared Huawei colors in OperatorX and ubenchX across a live theme switch', () => {
const ascend = normalizeBundle(run('201', 'ascend950pr-external', [64, 128]));
const nvidia = normalizeBundle(run('202', 'h200-dgxc', [64, 128]));
for (const dataset of [ascend, nvidia]) {
for (const row of dataset.results) {
const m = Number(row.args.m);
row.args = { ...row.args, n: 4096, k: 4096 };
row.shape = `${m} × 4096 × 4096`;
row.latencyUs = 100;
row.tflops = (2 * m * 4096 * 4096) / (100 * 1e6);
}
}
const view = comparisonView(
buildComparison('gemm', [
{ runner: 'ascend950pr-external', dataset: ascend },
{ runner: 'h200-dgxc', dataset: nvidia },
]),
null,
);
cy.intercept('GET', '/api/v1/operatorx/compare?op=gemm*', view).as('colorComparison');
const visitOptions = {
onBeforeLoad(win: Cypress.AUTWindow) {
win.localStorage.setItem('theme', 'light');
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
},
};
cy.visit('/operatorx', visitOptions);
cy.wait('@colorComparison');
cy.get('[data-testid="operatorx-viz-roofline"]').scrollIntoView();
operatorPoints().first().should('have.css', 'fill', 'rgb(148, 106, 0)');
cy.contains('[data-testid="operatorx-coverage"] li', '950PR')
.find('.rounded-full')
.first()
.should('have.css', 'background-color', 'rgb(148, 106, 0)');
cy.get('[data-testid="operatorx-viz-roofline"]').screenshot('huawei-operatorx-light');
cy.get('[data-testid="theme-toggle"]').click();
cy.get('html').should('have.class', 'dark');
operatorPoints().first().should('have.css', 'fill', 'rgb(235, 192, 74)');
cy.contains('[data-testid="operatorx-coverage"] li', '950PR')
.find('.rounded-full')
.first()
.should('have.css', 'background-color', 'rgb(235, 192, 74)');
cy.get('[data-testid="operatorx-viz-roofline"]').screenshot('huawei-operatorx-dark');

cy.visit('/ubenchx/mem-bw', visitOptions);
bandwidthPoints().first().should('have.css', 'fill', 'rgb(148, 106, 0)');
cy.get('[data-testid="theme-toggle"]').click();
bandwidthPoints().first().should('have.css', 'fill', 'rgb(235, 192, 74)');
bandwidthPoints().eq(20).trigger('mouseenter', { force: true });
cy.get('[data-chart-tooltip="ubenchx-mbu"]')
.should('contain', 'Huawei 950PR')
.find('span')
.first()
.should('have.css', 'background-color', 'rgb(235, 192, 74)');
cy.get('#ubenchx-mbu').screenshot('huawei-ubenchx-dark');
});
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,15 @@ describe('Ascend OperatorX reference', () => {
});

it.each(['light', 'dark'] as const)(
'uses a separate crimson without recoloring AMD in %s mode',
'uses the shared Huawei palette without recoloring AMD in %s mode',
(theme) => {
const otherKeys = ['mi300x', 'mi325x', 'mi355x', 'h100', 'unknown'];
const colors = operatorXColors([...otherKeys, 'ascend950pr'], theme);
for (const key of otherKeys)
expect(colors[key]).toBe(generateVendorColors(otherKeys, theme)[key]);
expect(colors.ascend950pr).toMatch(/ 355\)$/u);
expect(colors.ascend950pr).toBe(
generateVendorColors(['Huawei 950PR'], theme)['Huawei 950PR'],
);
expect(colors.ascend950pr).not.toBe(colors.mi355x);
expect(operatorXColors(['ascend950pr'], theme).ascend950pr).toBe(colors.ascend950pr);
expect(operatorXColors([], theme)).toEqual({});
Expand Down
10 changes: 1 addition & 9 deletions packages/app/src/components/operatorx/compare/hardware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,8 @@ export function hardwareLabel(key: string): string {
return OPERATORX_HARDWARE_REFERENCES[key]?.label ?? HW_REGISTRY[key]?.label ?? key.toUpperCase();
}

/** Cool crimson for Ascend, separate from AMD's warm red/orange hue range (12–42). */
export function operatorXColors(keys: string[], theme: 'light' | 'dark'): Record<string, string> {
const colors = generateVendorColors(
keys.filter((key) => key !== 'ascend950pr'),
theme,
);
if (keys.includes('ascend950pr')) {
colors.ascend950pr = theme === 'dark' ? 'oklch(0.72 0.20 355)' : 'oklch(0.48 0.19 355)';
}
return colors;
return generateVendorColors(keys, theme);
}

export function sortHardware(keys: string[]): string[] {
Expand Down
49 changes: 40 additions & 9 deletions packages/app/src/components/ubenchx/UbenchxContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import * as d3 from 'd3';
import { useMemo, useState } from 'react';
import { useTheme } from 'next-themes';
import { isDarkTheme } from '@/lib/themes/themes';
import { generateVendorColors, getVendor } from '@/lib/charts/dynamic-colors';

import {
Select,
Expand Down Expand Up @@ -32,7 +35,6 @@ const GPU_COLORS: Record<string, string> = {
MI355X: 'oklch(0.62 0.20 18)',
MI325X: 'oklch(0.55 0.20 28)',
MI300X: 'oklch(0.48 0.20 38)',
'Huawei 950PR': '#C7000B',
};

const STRINGS = {
Expand Down Expand Up @@ -154,8 +156,13 @@ function paddedDomain(values: number[]): [number, number] {
const tooltipLine = (label: string, value: string) =>
`<div style="color: var(--muted-foreground); font-size: 11px; margin-bottom: 4px;"><strong>${label}:</strong> ${value}</div>`;

function formatTooltip(point: ChartPoint, isPinned: boolean, t: (typeof STRINGS)[Locale]): string {
const color = GPU_COLORS[point.gpuKey] ?? '#888';
function formatTooltip(
point: ChartPoint,
isPinned: boolean,
t: (typeof STRINGS)[Locale],
colors: Record<string, string>,
): string {
const color = colors[point.gpuKey] ?? '#888';
return `
<div style="background: var(--popover); border: 1px solid var(--border); border-radius: 8px; padding: 12px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); user-select: ${isPinned ? 'text' : 'none'};">
${isPinned ? `<div style="color: var(--muted-foreground); font-size: 10px; margin-bottom: 6px; font-style: italic;">${t.dismiss}</div>` : ''}
Expand All @@ -171,14 +178,20 @@ function formatTooltip(point: ChartPoint, isPinned: boolean, t: (typeof STRINGS)
`;
}

function GpuLegend({ gpuKeys }: { gpuKeys: readonly string[] }) {
function GpuLegend({
gpuKeys,
colors,
}: {
gpuKeys: readonly string[];
colors: Record<string, string>;
}) {
return (
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
{gpuKeys.map((key) => (
<span key={key} className="inline-flex items-center gap-1.5">
<span
className="inline-block h-2.5 w-2.5 rounded-full shrink-0"
style={{ backgroundColor: GPU_COLORS[key] ?? '#888' }}
style={{ backgroundColor: colors[key] ?? '#888' }}
/>
{key}
</span>
Expand All @@ -194,13 +207,17 @@ function UbenchxChart({
yLabel,
yScaleType,
locale,
colors,
theme,
}: {
chartId: string;
allPoints: ChartPoint[];
gpuKeys: readonly string[];
yLabel: string;
yScaleType: 'log' | 'linear';
locale: Locale;
colors: Record<string, string>;
theme: 'light' | 'dark';
}) {
const t = STRINGS[locale];

Expand Down Expand Up @@ -240,6 +257,7 @@ function UbenchxChart({
return (
<D3Chart<ChartPoint>
chartId={chartId}
displayIdentity={theme}
data={allPoints}
height={440}
margin={{ top: 24, right: 20, bottom: 62, left: 78 }}
Expand Down Expand Up @@ -272,7 +290,7 @@ function UbenchxChart({
key: `ubenchx-line-${chartId}`,
lines,
config: {
getColor: (key) => GPU_COLORS[key] ?? '#888',
getColor: (key) => colors[key] ?? '#888',
strokeWidth: 2.25,
curve: d3.curveMonotoneX,
},
Expand All @@ -286,7 +304,7 @@ function UbenchxChart({
getCy: () => 0,
getX: (point) => point.x,
getY: (point) => point.y,
getColor: (point) => GPU_COLORS[point.gpuKey] ?? '#888',
getColor: (point) => colors[point.gpuKey] ?? '#888',
getRadius: () => 3.5,
stroke: 'var(--background)',
strokeWidth: 1,
Expand All @@ -304,13 +322,24 @@ function UbenchxChart({
getRulerX: (point, scale) => (scale as d3.ScaleLogarithmic<number, number>)(point.x),
getRulerY: (point, scale) => scale(point.y),
attachToLayer: 1,
content: (point, isPinned) => formatTooltip(point, isPinned, STRINGS[locale]),
content: (point, isPinned) => formatTooltip(point, isPinned, STRINGS[locale], colors),
}}
/>
);
}

export function UbenchxContent() {
const theme = isDarkTheme(useTheme().resolvedTheme) ? 'dark' : 'light';
const colors = useMemo(
() => ({
...GPU_COLORS,
...generateVendorColors(
UBENCHX_GPU_KEYS.filter((key) => getVendor(key) === 'huawei'),
theme,
),
}),
[theme],
);
const locale = useLocale();
const t = STRINGS[locale];

Expand Down Expand Up @@ -377,7 +406,7 @@ export function UbenchxContent() {
</Select>
</div>

<GpuLegend gpuKeys={gpuKeys} />
<GpuLegend gpuKeys={gpuKeys} colors={colors} />

<section>
<h2 className="text-lg font-semibold mb-2">{t[metric.title]}</h2>
Expand All @@ -390,6 +419,8 @@ export function UbenchxContent() {
yLabel={t[metric.label]}
yScaleType={metric.scale}
locale={locale}
colors={colors}
theme={theme}
/>
</section>

Expand Down
30 changes: 30 additions & 0 deletions packages/app/src/lib/charts/chart-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { hsl } from 'd3';
import { generateVendorColors } from './dynamic-colors';
import { describe, it, expect, vi } from 'vitest';

import iwanthue from 'iwanthue';
Expand Down Expand Up @@ -1579,3 +1581,31 @@ describe('metricChartTitle', () => {
expect(metricChartTitle(bare, 'y_tpPerGpu', 'en')).toBe('Token Throughput per Chip');
});
});

it.each(['light', 'dark'] as const)(
'keeps Huawei high-contrast colors consistent and amber (%s)',
(theme) => {
const one = generateHighContrastColors(['ascend950pr', 'mi355x', 'h100'], theme);
expect(one.ascend950pr).toBe(generateVendorColors(['ascend950pr'], theme).ascend950pr);
const many = generateHighContrastColors(
['ascend950pr_bf16', 'ascend950pr_fp8', 'ascend910b3'],
theme,
);
expect(new Set(Object.values(many)).size).toBe(3);
for (const color of Object.values(many)) {
const [r, g, b] = parseRgb(color);
expect(r).toBeGreaterThan(b);
expect(g).toBeGreaterThan(b);
}
},
);

it('reserves amber from other high-contrast series when Huawei is present, including cached palettes', () => {
const other = [...Array.from({ length: 8 }, (_, i) => `nvidia_series${i}`), 'amd_series'];
generateHighContrastColors(other, 'dark');
const colors = generateHighContrastColors([...other, 'ascend950pr'], 'dark');
for (const key of other) {
const hue = hsl(colors[key]).h;
expect(hue < 35 || hue > 65, `${key}: ${colors[key]}`).toBe(true);
}
});
42 changes: 31 additions & 11 deletions packages/app/src/lib/charts/chart-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { GOOGLE_BLUE, resolveFrameworkAlias } from '@semianalysisai/inferencex-constants';
import {
GOOGLE_BLUE,
HUAWEI_CHART_COLORS,
HUAWEI_CHART_PALETTE,
resolveFrameworkAlias,
} from '@semianalysisai/inferencex-constants';
import iwanthue from 'iwanthue';
import { chartPaletteTheme, isDarkTheme } from '@/lib/themes/themes';

Expand All @@ -23,6 +28,7 @@ const BANNED_HUE_TEST: Record<Vendor, ((hue: number) => boolean) | null> = {
amd: (hue) => hue >= 120 && hue <= 195,
openai: (hue) => hue < 290 || hue > 350,
google: (hue) => hue < 255 || hue > 300,
huawei: null,
unknown: null,
};

Expand All @@ -34,6 +40,7 @@ const PREFERRED_ZONE: Record<
amd: { hmin: 20, hmax: 50, cmin: 70, lmin: 50 },
openai: { hmin: 300, hmax: 340 },
google: { hmin: 265, hmax: 295 },
huawei: HUAWEI_CHART_PALETTE.lab,
unknown: null,
};

Expand Down Expand Up @@ -66,21 +73,28 @@ export const generateHighContrastColors = (
}

const multiVendor = groups.size > 1;
const reserveHuawei = groups.has('huawei');

for (const [vendor, vendorKeys] of groups) {
const count = vendorKeys.length;
if (vendor === 'huawei' && count === 1) {
colors[vendorKeys[0]] = HUAWEI_CHART_COLORS[isDarkTheme(theme) ? 'dark' : 'light'];
continue;
}
if (vendor === 'google' && count === 1) {
colors[vendorKeys[0]] = GOOGLE_BLUE;
continue;
}
const isBanned = BANNED_HUE_TEST[vendor] ?? null;
const preferred = PREFERRED_ZONE[vendor] ?? null;

const usePreferred = multiVendor && preferred && count <= PREFERRED_MAX;
const usePreferred =
preferred && ((multiVendor && count <= PREFERRED_MAX) || vendor === 'huawei');
const useBan = multiVendor && !usePreferred && isBanned && count <= BAN_MAX;

const excludeAmber = reserveHuawei && vendor !== 'huawei';
const mode = usePreferred ? 'pref' : useBan ? 'ban' : 'open';
const cacheKey = `${vendor}|${theme}|${count}|${mode}|${lmin}|${lmax}`;
const cacheKey = `${vendor}|${theme}|${count}|${mode}|${lmin}|${lmax}|${excludeAmber}`;

let palette = PALETTE_CACHE.get(cacheKey);
if (!palette) {
Expand All @@ -95,14 +109,20 @@ export const generateHighContrastColors = (
lmax,
}
: { hmin: 0, hmax: 360, cmin: 30, cmax: 100, lmin, lmax },
...(useBan &&
isBanned && {
colorFilter: (_rgb: [number, number, number], lab: [number, number, number]) => {
if (lab[0] < lmin || lab[0] > lmax) return false;
const hue = ((Math.atan2(lab[2], lab[1]) * 180) / Math.PI + 360) % 360;
return !isBanned(hue);
},
}),
...((useBan || excludeAmber) && {
colorFilter: (_rgb: [number, number, number], lab: [number, number, number]) => {
if (lab[0] < lmin || lab[0] > lmax) return false;
const hue = ((Math.atan2(lab[2], lab[1]) * 180) / Math.PI + 360) % 360;
return (
!(useBan && isBanned?.(hue)) &&
!(
excludeAmber &&
hue >= HUAWEI_CHART_PALETTE.reservedLab.hmin &&
hue <= HUAWEI_CHART_PALETTE.reservedLab.hmax
)
);
},
}),
seed: `${vendor}-${theme}`,
clustering: 'force-vector',
quality: 50,
Expand Down
Loading
Loading