Rename dev component, separate rooflineHeatmap

- moved roofline helper functions to utils
This commit is contained in:
Christoph Kluge 2023-09-05 10:01:34 +02:00
parent f235b1a99c
commit b449b77b95
6 changed files with 532 additions and 667 deletions

View File

@ -10,7 +10,7 @@
import { binsFromFootprint } from './utils.js' import { binsFromFootprint } from './utils.js'
import ScatterPlot from './plots/Scatter.svelte' import ScatterPlot from './plots/Scatter.svelte'
import PlotTable from './PlotTable.svelte' import PlotTable from './PlotTable.svelte'
import Roofline from './plots/Roofline.svelte' import RooflineHeatmap from './plots/RooflineHeatmap.svelte'
const { query: initq } = init() const { query: initq } = init()
@ -315,7 +315,7 @@
{:else if $rooflineQuery.data && cluster} {:else if $rooflineQuery.data && cluster}
<div bind:clientWidth={colWidth2}> <div bind:clientWidth={colWidth2}>
{#key $rooflineQuery.data} {#key $rooflineQuery.data}
<Roofline <RooflineHeatmap
width={colWidth2} height={300} width={colWidth2} height={300}
tiles={$rooflineQuery.data.rooflineHeatmap} tiles={$rooflineQuery.data.rooflineHeatmap}
cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null} cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null}

View File

@ -1,8 +1,7 @@
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import Refresher from "./joblist/Refresher.svelte"; import Refresher from "./joblist/Refresher.svelte";
import Roofline, { transformPerNodeData } from "./plots/Roofline.svelte"; import Roofline from "./plots/Roofline.svelte";
import Rooflineuplot from "./plots/Rooflineuplot.svelte";
import Pie, { colors } from "./plots/Pie.svelte"; import Pie, { colors } from "./plots/Pie.svelte";
import Histogram from "./plots/Histogram.svelte"; import Histogram from "./plots/Histogram.svelte";
import { import {
@ -17,7 +16,7 @@
Progress, Progress,
Icon, Icon,
} from "sveltestrap"; } from "sveltestrap";
import { init, convert2uplot } from "./utils.js"; import { init, convert2uplot, transformPerNodeDataForRoofline } from "./utils.js";
import { scaleNumbers } from "./units.js"; import { scaleNumbers } from "./units.js";
import { import {
queryStore, queryStore,
@ -33,8 +32,7 @@
let plotWidths = [], let plotWidths = [],
colWidth1 = 0, colWidth1 = 0,
colWidth2 = 0, colWidth2 = 0
colWidth3 = 0;
let from = new Date(Date.now() - 5 * 60 * 1000), let from = new Date(Date.now() - 5 * 60 * 1000),
to = new Date(Date.now()); to = new Date(Date.now());
const topOptions = [ const topOptions = [
@ -434,7 +432,7 @@
colorDots={true} colorDots={true}
showTime={false} showTime={false}
cluster={subCluster} cluster={subCluster}
data={transformPerNodeData( data={transformPerNodeDataForRoofline(
$mainQuery.data.nodeMetrics.filter( $mainQuery.data.nodeMetrics.filter(
(data) => data.subCluster == subCluster.name (data) => data.subCluster == subCluster.name
) )
@ -667,13 +665,4 @@
{/key} {/key}
</Col> </Col>
</Row> </Row>
<Row>
<Col>
<div bind:clientWidth={colWidth3}>
<Rooflineuplot
width={colWidth3 - 25}
cluster={$initq.data.clusters.find((c) => c.name == cluster).subClusters[0]}
/>
</Col>
</Row>
{/if} {/if}

View File

@ -1,16 +1,72 @@
<div class="cc-plot"> <script>
<canvas bind:this={canvasElement} width="{prevWidth}" height="{prevHeight}"></canvas> import uPlot from 'uplot'
</div> import { formatNumber } from '../units.js'
import { onMount, onDestroy } from 'svelte'
import { Card } from 'sveltestrap'
<script context="module"> export let flopsAny = null
const axesColor = '#aaaaaa' export let memBw = null
const tickFontSize = 10 export let maxY = null
const labelFontSize = 12 export let cluster = null
const fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"' export let width = 500
const paddingLeft = 40, export let height = 300
paddingRight = 10, export let renderTime = false
paddingTop = 10, export let data = null
paddingBottom = 50
let plotWrapper = null
let uplot = null
let timeoutId = null
// Three Render-Cases:
// #1 Single-Job Roofline -> Has Time-Information: Use data, allow renderTime
// #2 MultiNode Roofline - > Has No Time-Information: Transform from nodeData, only "IST"-state of nodes, no timeInfo
// #3 Multi-Job Roofline as Heatmap -> Keep Original
// Start Demo Data
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randFloat(min, max) {
return roundTwo(((Math.random() * (max - min + 1)) + min) / randInt(1, 500));
}
function roundTwo(num) {
return Math.round((num + Number.EPSILON) * 100) / 100
}
function filledArr(len, val, time) {
let arr = Array(len);
if (typeof val == "function") {
for (let i = 0; i < len; ++i)
arr[i] = val(i);
}
else if (time) {
for (let i = 0; i < len; ++i)
arr[i] = i / 1000;
}
else {
for (let i = 0; i < len; ++i)
arr[i] = i;
}
return arr;
}
let points = 1000;
data = [null, []] // Null-Axis required for scatter
data[1][0] = filledArr(points, i => randFloat(1,5000), false) // Intensity
data[1][1] = filledArr(points, i => randFloat(1,5000), false) // Performance
// data[1][0] = filledArr(points, 0, false) // Intensity
// data[1][1] = filledArr(points, 0, false) // Performance
data[2] = filledArr(points, 0, true) // Time Information (Optional)
// End Demo Data
// Helpers
function getGradientR(x) { function getGradientR(x) {
if (x < 0.5) return 0 if (x < 0.5) return 0
@ -37,6 +93,10 @@
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})` return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`
} }
function nearestThousand (num) {
return Math.ceil(num/1000) * 1000
}
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) { function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l
@ -46,313 +106,192 @@
} }
} }
function axisStepFactor(i, size) { // End Helpers
if (size && size < 500)
return 10
if (i % 3 == 0) const drawColorPoints = (u, seriesIdx, idx0, idx1) => {
return 2 const size = 5 * devicePixelRatio;
else if (i % 3 == 1)
return 2.5
else
return 2
}
function render(ctx, data, cluster, width, height, colorDots, showTime, defaultMaxY) { uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
if (width <= 0) let d = u.data[seriesIdx];
return let deg360 = 2 * Math.PI;
for (let i = 0; i < d[0].length; i++) {
let p = new Path2D();
let xVal = d[0][i];
let yVal = d[1][i];
u.ctx.strokeStyle = getRGB(u.data[2][i])
u.ctx.fillStyle = getRGB(u.data[2][i])
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
let cx = valToPosX(xVal, scaleX, xDim, xOff);
let cy = valToPosY(yVal, scaleY, yDim, yOff);
const [minX, maxX, minY, maxY] = [0.01, 1000, 1., cluster?.flopRateSimd?.value || defaultMaxY] p.moveTo(cx + size/2, cy);
const w = width - paddingLeft - paddingRight arc(p, cx, cy, size/2, 0, deg360);
const h = height - paddingTop - paddingBottom }
u.ctx.fill(p);
// Helpers:
const [log10minX, log10maxX, log10minY, log10maxY] =
[Math.log10(minX), Math.log10(maxX), Math.log10(minY), Math.log10(maxY)]
/* Value -> Pixel-Coordinate */
const getCanvasX = (x) => {
x = Math.log10(x)
x -= log10minX; x /= (log10maxX - log10minX)
return Math.round((x * w) + paddingLeft)
}
const getCanvasY = (y) => {
y = Math.log10(y)
y -= log10minY
y /= (log10maxY - log10minY)
return Math.round((h - y * h) + paddingTop)
}
// Axes
ctx.fillStyle = 'black'
ctx.strokeStyle = axesColor
ctx.font = `${tickFontSize}px ${fontFamily}`
ctx.beginPath()
for (let x = minX, i = 0; x <= maxX; i++) {
let px = getCanvasX(x)
let text = formatNumber(x)
let textWidth = ctx.measureText(text).width
ctx.fillText(text,
Math.floor(px - (textWidth / 2)),
height - paddingBottom + tickFontSize + 5)
ctx.moveTo(px, paddingTop - 5)
ctx.lineTo(px, height - paddingBottom + 5)
x *= axisStepFactor(i, w)
}
if (data.xLabel) {
ctx.font = `${labelFontSize}px ${fontFamily}`
let textWidth = ctx.measureText(data.xLabel).width
ctx.fillText(data.xLabel, Math.floor((width / 2) - (textWidth / 2)), height - 20)
}
ctx.textAlign = 'center'
ctx.font = `${tickFontSize}px ${fontFamily}`
for (let y = minY, i = 0; y <= maxY; i++) {
let py = getCanvasY(y)
ctx.moveTo(paddingLeft - 5, py)
ctx.lineTo(width - paddingRight + 5, py)
ctx.save()
ctx.translate(paddingLeft - 10, py)
ctx.rotate(-Math.PI / 2)
ctx.fillText(formatNumber(y), 0, 0)
ctx.restore()
y *= axisStepFactor(i)
}
if (data.yLabel) {
ctx.font = `${labelFontSize}px ${fontFamily}`
ctx.save()
ctx.translate(15, Math.floor(height / 2))
ctx.rotate(-Math.PI / 2)
ctx.fillText(data.yLabel, 0, 0)
ctx.restore()
}
ctx.stroke()
// Draw Data
if (data.x && data.y) {
for (let i = 0; i < data.x.length; i++) {
let x = data.x[i], y = data.y[i], c = data.c[i]
if (x == null || y == null || Number.isNaN(x) || Number.isNaN(y))
continue
const s = 3
const px = getCanvasX(x)
const py = getCanvasY(y)
ctx.fillStyle = getRGB(c)
ctx.beginPath()
ctx.arc(px, py, s, 0, Math.PI * 2, false)
ctx.fill()
} }
} else if (data.tiles) { });
const rows = data.tiles.length
const cols = data.tiles[0].length
const tileWidth = Math.ceil(w / cols) return null;
const tileHeight = Math.ceil(h / rows) };
let max = data.tiles.reduce((max, row) => const drawPoints = (u, seriesIdx, idx0, idx1) => {
Math.max(max, row.reduce((max, val) => const size = 5 * devicePixelRatio;
Math.max(max, val)), 0), 0)
if (max == 0) uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
max = 1 let d = u.data[seriesIdx];
u.ctx.strokeStyle = getRGB(0);
const tileColor = val => `rgba(255, 0, 0, ${(val / max)})` u.ctx.fillStyle = getRGB(0);
let deg360 = 2 * Math.PI;
for (let i = 0; i < rows; i++) { let p = new Path2D();
for (let j = 0; j < cols; j++) { for (let i = 0; i < d[0].length; i++) {
let px = paddingLeft + (j / cols) * w let xVal = d[0][i];
let py = paddingTop + (h - (i / rows) * h) - tileHeight let yVal = d[1][i];
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
ctx.fillStyle = tileColor(data.tiles[i][j]) let cx = valToPosX(xVal, scaleX, xDim, xOff);
ctx.fillRect(px, py, tileWidth, tileHeight) let cy = valToPosY(yVal, scaleY, yDim, yOff);
p.moveTo(cx + size/2, cy);
arc(p, cx, cy, size/2, 0, deg360);
} }
} }
} u.ctx.fill(p);
});
return null;
};
// Draw roofs function render() {
ctx.strokeStyle = 'black' const opts = {
ctx.lineWidth = 2 title: "",
ctx.beginPath() mode: 2,
if (cluster != null) { width: width,
const ycut = 0.01 * cluster.memoryBandwidth.value height: height,
const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value legend: {
const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value show: false
const scalarKneeX = getCanvasX(scalarKnee), },
simdKneeX = getCanvasX(simdKnee), cursor: { drag: { x: false, y: false } },
flopRateScalarY = getCanvasY(cluster.flopRateScalar.value), axes: [
flopRateSimdY = getCanvasY(cluster.flopRateSimd.value) { label: 'Intensity [FLOPS/Byte]' },
{ label: 'Performace [GFLOPS]' }
],
scales: {
x: {
time: false,
range: [0.01, 1000],
distr: 3, // Render as log
log: 10, // log exp
},
y: {
range: [1.0, nearestThousand(cluster.flopRateSimd.value || maxY)],
distr: 3, // Render as log
log: 10, // log exp
},
},
series: [
{},
{ paths: renderTime ? drawColorPoints : drawPoints }
],
hooks: {
drawClear: [
u => {
u.series.forEach((s, i) => {
if (i > 0)
s._paths = null;
});
},
],
draw: [
u => { // draw roofs when cluster set
// console.log(u)
if (cluster != null) {
const padding = u._padding // [top, right, bottom, left]
if (scalarKneeX < width - paddingRight) { u.ctx.strokeStyle = 'black'
ctx.moveTo(scalarKneeX, flopRateScalarY) u.ctx.lineWidth = 2
ctx.lineTo(width - paddingRight, flopRateScalarY) u.ctx.beginPath()
}
if (simdKneeX < width - paddingRight) { const ycut = 0.01 * cluster.memoryBandwidth.value
ctx.moveTo(simdKneeX, flopRateSimdY) const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value
ctx.lineTo(width - paddingRight, flopRateSimdY) const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value
} const scalarKneeX = u.valToPos(scalarKnee, 'x', true), // Value, axis, toCanvasPixels
simdKneeX = u.valToPos(simdKnee, 'x', true),
flopRateScalarY = u.valToPos(cluster.flopRateScalar.value, 'y', true),
flopRateSimdY = u.valToPos(cluster.flopRateSimd.value, 'y', true)
let x1 = getCanvasX(0.01), if (scalarKneeX < width - padding[1]) { // Top horizontal roofline
y1 = getCanvasY(ycut), u.ctx.moveTo(scalarKneeX, flopRateScalarY)
x2 = getCanvasX(simdKnee), u.ctx.lineTo(width - padding[1], flopRateScalarY)
y2 = flopRateSimdY }
let xAxisIntersect = lineIntersect( if (simdKneeX < width - padding[1]) { // Lower horitontal roofline
x1, y1, x2, y2, u.ctx.moveTo(simdKneeX, flopRateSimdY)
0, height - paddingBottom, width, height - paddingBottom) u.ctx.lineTo(width - padding[1], flopRateSimdY)
}
if (xAxisIntersect.x > x1) { let x1 = u.valToPos(0.01, 'x', true),
x1 = xAxisIntersect.x y1 = u.valToPos(ycut, 'y', true)
y1 = xAxisIntersect.y
}
ctx.moveTo(x1, y1) let x2 = u.valToPos(simdKnee, 'x', true),
ctx.lineTo(x2, y2) y2 = flopRateSimdY
}
ctx.stroke()
if (colorDots && showTime && data.x && data.y) { let xAxisIntersect = lineIntersect(
// The Color Scale For Time Information x1, y1, x2, y2,
ctx.fillStyle = 'black' u.valToPos(0.01, 'x', true), u.valToPos(1.0, 'y', true), // X-Axis Start Coords
ctx.fillText('Time:', 17, height - 5) u.valToPos(1000, 'x', true), u.valToPos(1.0, 'y', true) // X-Axis End Coords
const start = paddingLeft + 5 )
for (let x = start; x < width - paddingRight; x += 15) {
let c = (x - start) / (width - start - paddingRight) if (xAxisIntersect.x > x1) {
ctx.fillStyle = getRGB(c) x1 = xAxisIntersect.x
ctx.beginPath() y1 = xAxisIntersect.y
ctx.arc(x, height - 10, 5, 0, Math.PI * 2, false) }
ctx.fill()
} // Diagonal
} u.ctx.moveTo(x1, y1)
u.ctx.lineTo(x2, y2)
u.ctx.stroke()
// Reset grid lineWidth
u.ctx.lineWidth = 0.15
}
}
]
},
};
uplot = new uPlot(opts, data, plotWrapper);
} }
function transformData(flopsAny, memBw, colorDots) { // Uses Metric Object // Copied from Histogram
const nodes = flopsAny.series.length
const timesteps = flopsAny.series[0].data.length
/* c will contain values from 0 to 1 representing the time */
const x = [], y = [], c = []
if (flopsAny && memBw) {
for (let i = 0; i < nodes; i++) {
const flopsData = flopsAny.series[i].data
const memBwData = memBw.series[i].data
for (let j = 0; j < timesteps; j++) {
const f = flopsData[j], m = memBwData[j]
const intensity = f / m
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
continue
x.push(intensity)
y.push(f)
c.push(colorDots ? j / timesteps : 0)
}
}
} else {
console.warn("transformData: metrics for 'mem_bw' and/or 'flops_any' missing!")
}
return {
x, y, c,
xLabel: 'Intensity [FLOPS/byte]',
yLabel: 'Performance [GFLOPS]'
}
}
// Return something to be plotted. The argument shall be the result of the
// `nodeMetrics` GraphQL query.
export function transformPerNodeData(nodes) {
const x = [], y = [], c = []
for (let node of nodes) {
let flopsAny = node.metrics.find(m => m.name == 'flops_any' && m.scope == 'node')?.metric
let memBw = node.metrics.find(m => m.name == 'mem_bw' && m.scope == 'node')?.metric
if (!flopsAny || !memBw) {
console.warn("transformPerNodeData: metrics for 'mem_bw' and/or 'flops_any' missing!")
continue
}
let flopsData = flopsAny.series[0].data, memBwData = memBw.series[0].data
const f = flopsData[flopsData.length - 1], m = memBwData[flopsData.length - 1]
const intensity = f / m
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
continue
x.push(intensity)
y.push(f)
c.push(0)
}
return {
x, y, c,
xLabel: 'Intensity [FLOPS/byte]',
yLabel: 'Performance [GFLOPS]'
}
}
</script>
<script>
import { onMount, tick } from 'svelte'
import { formatNumber } from '../units.js'
export let flopsAny = null
export let memBw = null
export let cluster = null
export let maxY = null
export let width = 500
export let height = 300
export let tiles = null
export let colorDots = true
export let showTime = true
export let data = null
console.assert(data || tiles || (flopsAny && memBw), "you must provide flopsAny and memBw or tiles!")
let ctx, canvasElement, prevWidth = width, prevHeight = height
data = data != null ? data : (flopsAny && memBw
? transformData(flopsAny.metric, memBw.metric, colorDots) // Use Metric Object from Parent
: {
tiles: tiles,
xLabel: 'Intensity [FLOPS/byte]',
yLabel: 'Performance [GFLOPS]'
})
onMount(() => { onMount(() => {
ctx = canvasElement.getContext('2d') render()
if (prevWidth != width || prevHeight != height) {
sizeChanged()
return
}
canvasElement.width = width
canvasElement.height = height
render(ctx, data, cluster, width, height, colorDots, showTime, maxY)
}) })
let timeoutId = null onDestroy(() => {
function sizeChanged() { if (uplot)
if (!ctx) uplot.destroy()
return
if (timeoutId != null) if (timeoutId != null)
clearTimeout(timeoutId) clearTimeout(timeoutId)
})
function sizeChanged() {
if (timeoutId != null)
clearTimeout(timeoutId)
prevWidth = width
prevHeight = height
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
if (!canvasElement)
return
timeoutId = null timeoutId = null
canvasElement.width = width if (uplot)
canvasElement.height = height uplot.destroy()
render(ctx, data, cluster, width, height, colorDots, showTime, maxY)
}, 250) render()
}, 200)
} }
$: sizeChanged(width, height) $: sizeChanged(width, height)
</script> </script>
{#if data != null}
<div bind:this={plotWrapper}/>
{:else}
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card>
{/if}

View File

@ -0,0 +1,234 @@
<div class="cc-plot">
<canvas bind:this={canvasElement} width="{prevWidth}" height="{prevHeight}"></canvas>
</div>
<script context="module">
const axesColor = '#aaaaaa'
const tickFontSize = 10
const labelFontSize = 12
const fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
const paddingLeft = 40,
paddingRight = 10,
paddingTop = 10,
paddingBottom = 50
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l
return {
x: x1 + a * (x2 - x1),
y: y1 + a * (y2 - y1)
}
}
function axisStepFactor(i, size) {
if (size && size < 500)
return 10
if (i % 3 == 0)
return 2
else if (i % 3 == 1)
return 2.5
else
return 2
}
function render(ctx, data, cluster, width, height, defaultMaxY) {
if (width <= 0)
return
const [minX, maxX, minY, maxY] = [0.01, 1000, 1., cluster?.flopRateSimd?.value || defaultMaxY]
const w = width - paddingLeft - paddingRight
const h = height - paddingTop - paddingBottom
// Helpers:
const [log10minX, log10maxX, log10minY, log10maxY] =
[Math.log10(minX), Math.log10(maxX), Math.log10(minY), Math.log10(maxY)]
/* Value -> Pixel-Coordinate */
const getCanvasX = (x) => {
x = Math.log10(x)
x -= log10minX; x /= (log10maxX - log10minX)
return Math.round((x * w) + paddingLeft)
}
const getCanvasY = (y) => {
y = Math.log10(y)
y -= log10minY
y /= (log10maxY - log10minY)
return Math.round((h - y * h) + paddingTop)
}
// Axes
ctx.fillStyle = 'black'
ctx.strokeStyle = axesColor
ctx.font = `${tickFontSize}px ${fontFamily}`
ctx.beginPath()
for (let x = minX, i = 0; x <= maxX; i++) {
let px = getCanvasX(x)
let text = formatNumber(x)
let textWidth = ctx.measureText(text).width
ctx.fillText(text,
Math.floor(px - (textWidth / 2)),
height - paddingBottom + tickFontSize + 5)
ctx.moveTo(px, paddingTop - 5)
ctx.lineTo(px, height - paddingBottom + 5)
x *= axisStepFactor(i, w)
}
if (data.xLabel) {
ctx.font = `${labelFontSize}px ${fontFamily}`
let textWidth = ctx.measureText(data.xLabel).width
ctx.fillText(data.xLabel, Math.floor((width / 2) - (textWidth / 2)), height - 20)
}
ctx.textAlign = 'center'
ctx.font = `${tickFontSize}px ${fontFamily}`
for (let y = minY, i = 0; y <= maxY; i++) {
let py = getCanvasY(y)
ctx.moveTo(paddingLeft - 5, py)
ctx.lineTo(width - paddingRight + 5, py)
ctx.save()
ctx.translate(paddingLeft - 10, py)
ctx.rotate(-Math.PI / 2)
ctx.fillText(formatNumber(y), 0, 0)
ctx.restore()
y *= axisStepFactor(i)
}
if (data.yLabel) {
ctx.font = `${labelFontSize}px ${fontFamily}`
ctx.save()
ctx.translate(15, Math.floor(height / 2))
ctx.rotate(-Math.PI / 2)
ctx.fillText(data.yLabel, 0, 0)
ctx.restore()
}
ctx.stroke()
// Draw Data
if (data.tiles) {
const rows = data.tiles.length
const cols = data.tiles[0].length
const tileWidth = Math.ceil(w / cols)
const tileHeight = Math.ceil(h / rows)
let max = data.tiles.reduce((max, row) =>
Math.max(max, row.reduce((max, val) =>
Math.max(max, val)), 0), 0)
if (max == 0)
max = 1
const tileColor = val => `rgba(255, 0, 0, ${(val / max)})`
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
let px = paddingLeft + (j / cols) * w
let py = paddingTop + (h - (i / rows) * h) - tileHeight
ctx.fillStyle = tileColor(data.tiles[i][j])
ctx.fillRect(px, py, tileWidth, tileHeight)
}
}
}
// Draw roofs
ctx.strokeStyle = 'black'
ctx.lineWidth = 2
ctx.beginPath()
if (cluster != null) {
const ycut = 0.01 * cluster.memoryBandwidth.value
const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value
const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value
const scalarKneeX = getCanvasX(scalarKnee),
simdKneeX = getCanvasX(simdKnee),
flopRateScalarY = getCanvasY(cluster.flopRateScalar.value),
flopRateSimdY = getCanvasY(cluster.flopRateSimd.value)
if (scalarKneeX < width - paddingRight) {
ctx.moveTo(scalarKneeX, flopRateScalarY)
ctx.lineTo(width - paddingRight, flopRateScalarY)
}
if (simdKneeX < width - paddingRight) {
ctx.moveTo(simdKneeX, flopRateSimdY)
ctx.lineTo(width - paddingRight, flopRateSimdY)
}
let x1 = getCanvasX(0.01),
y1 = getCanvasY(ycut),
x2 = getCanvasX(simdKnee),
y2 = flopRateSimdY
let xAxisIntersect = lineIntersect(
x1, y1, x2, y2,
0, height - paddingBottom, width, height - paddingBottom)
if (xAxisIntersect.x > x1) {
x1 = xAxisIntersect.x
y1 = xAxisIntersect.y
}
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
}
ctx.stroke()
}
</script>
<script>
import { onMount } from 'svelte'
import { formatNumber } from '../units.js'
export let cluster = null
export let tiles = null
export let maxY = null
export let width = 500
export let height = 300
console.assert(tiles, "you must provide tiles!")
let ctx, canvasElement, prevWidth = width, prevHeight = height
const data = {
tiles: tiles,
xLabel: 'Intensity [FLOPS/byte]',
yLabel: 'Performance [GFLOPS]'
}
onMount(() => {
ctx = canvasElement.getContext('2d')
if (prevWidth != width || prevHeight != height) {
sizeChanged()
return
}
canvasElement.width = width
canvasElement.height = height
render(ctx, data, cluster, width, height, maxY)
})
let timeoutId = null
function sizeChanged() {
if (!ctx)
return
if (timeoutId != null)
clearTimeout(timeoutId)
prevWidth = width
prevHeight = height
timeoutId = setTimeout(() => {
if (!canvasElement)
return
timeoutId = null
canvasElement.width = width
canvasElement.height = height
render(ctx, data, cluster, width, height, maxY)
}, 250)
}
$: sizeChanged(width, height)
</script>

View File

@ -1,360 +0,0 @@
<script>
import uPlot from 'uplot'
import { formatNumber } from '../units.js'
import { onMount, onDestroy } from 'svelte'
import { Card } from 'sveltestrap'
export let flopsAny = null
export let memBw = null
export let maxY = null
export let cluster = null
export let width = 500
export let height = 300
export let renderTime = false
export let data = null
let plotWrapper = null
let uplot = null
let timeoutId = null
// Three Render-Cases:
// #1 Single-Job Roofline -> Has Time-Information: Use data, allow colorDots && showTime
// #2 MultiNode Roofline - > Has No Time-Information: Transform from nodeData, only "IST"-state of nodes, no timeInfo
// #3 Multi-Job Roofline -> No Time Information? -> Use Backend-Prepared "Tiles" with increasing occupancy for stronger values
// Start Demo Data
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randFloat(min, max) {
return roundTwo(((Math.random() * (max - min + 1)) + min) / randInt(1, 500));
}
function roundTwo(num) {
return Math.round((num + Number.EPSILON) * 100) / 100
}
function filledArr(len, val, time) {
let arr = Array(len);
if (typeof val == "function") {
for (let i = 0; i < len; ++i)
arr[i] = val(i);
}
else if (time) {
for (let i = 0; i < len; ++i)
arr[i] = i / 1000;
}
else {
for (let i = 0; i < len; ++i)
arr[i] = i;
}
return arr;
}
let points = 1000;
data = [null, []] // Null-Axis required for scatter
data[1][0] = filledArr(points, i => randFloat(1,5000), false) // Intensity
data[1][1] = filledArr(points, i => randFloat(1,5000), false) // Performance
// data[1][0] = filledArr(points, 0, false) // Intensity
// data[1][1] = filledArr(points, 0, false) // Performance
data[2] = filledArr(points, 0, true) // Time Information (Optional)
// End Demo Data
// Helpers
function getGradientR(x) {
if (x < 0.5) return 0
if (x > 0.75) return 255
x = (x - 0.5) * 4.0
return Math.floor(x * 255.0)
}
function getGradientG(x) {
if (x > 0.25 && x < 0.75) return 255
if (x < 0.25) x = x * 4.0
else x = 1.0 - (x - 0.75) * 4.0
return Math.floor(x * 255.0)
}
function getGradientB(x) {
if (x < 0.25) return 255
if (x > 0.5) return 0
x = 1.0 - (x - 0.25) * 4.0
return Math.floor(x * 255.0)
}
function getRGB(c) {
return `rgb(${getGradientR(c)}, ${getGradientG(c)}, ${getGradientB(c)})`
}
function nearestThousand (num) {
return Math.ceil(num/1000) * 1000
}
function lineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
let l = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
let a = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / l
return {
x: x1 + a * (x2 - x1),
y: y1 + a * (y2 - y1)
}
}
function transformData(flopsAny, memBw, colorDots) { // Uses Metric Object
const nodes = flopsAny.series.length
const timesteps = flopsAny.series[0].data.length
/* c will contain values from 0 to 1 representing the time */
const x = [], y = [], c = []
if (flopsAny && memBw) {
for (let i = 0; i < nodes; i++) {
const flopsData = flopsAny.series[i].data
const memBwData = memBw.series[i].data
for (let j = 0; j < timesteps; j++) {
const f = flopsData[j], m = memBwData[j]
const intensity = f / m
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
continue
x.push(intensity)
y.push(f)
c.push(colorDots ? j / timesteps : 0)
}
}
} else {
console.warn("transformData: metrics for 'mem_bw' and/or 'flops_any' missing!")
}
return {
x, y, c,
xLabel: 'Intensity [FLOPS/byte]',
yLabel: 'Performance [GFLOPS]'
}
}
// Return something to be plotted. The argument shall be the result of the
// `nodeMetrics` GraphQL query.
export function transformPerNodeData(nodes) {
const x = [], y = [], c = []
for (let node of nodes) {
let flopsAny = node.metrics.find(m => m.name == 'flops_any' && m.scope == 'node')?.metric
let memBw = node.metrics.find(m => m.name == 'mem_bw' && m.scope == 'node')?.metric
if (!flopsAny || !memBw) {
console.warn("transformPerNodeData: metrics for 'mem_bw' and/or 'flops_any' missing!")
continue
}
let flopsData = flopsAny.series[0].data, memBwData = memBw.series[0].data
const f = flopsData[flopsData.length - 1], m = memBwData[flopsData.length - 1]
const intensity = f / m
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
continue
x.push(intensity)
y.push(f)
c.push(0)
}
return {
x, y, c,
xLabel: 'Intensity [FLOPS/byte]',
yLabel: 'Performance [GFLOPS]'
}
}
// End Helpers
const drawColorPoints = (u, seriesIdx, idx0, idx1) => {
const size = 5 * devicePixelRatio;
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
let d = u.data[seriesIdx];
let deg360 = 2 * Math.PI;
for (let i = 0; i < d[0].length; i++) {
let p = new Path2D();
let xVal = d[0][i];
let yVal = d[1][i];
u.ctx.strokeStyle = getRGB(u.data[2][i])
u.ctx.fillStyle = getRGB(u.data[2][i])
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
let cx = valToPosX(xVal, scaleX, xDim, xOff);
let cy = valToPosY(yVal, scaleY, yDim, yOff);
p.moveTo(cx + size/2, cy);
arc(p, cx, cy, size/2, 0, deg360);
}
u.ctx.fill(p);
}
});
return null;
};
const drawPoints = (u, seriesIdx, idx0, idx1) => {
const size = 5 * devicePixelRatio;
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
let d = u.data[seriesIdx];
u.ctx.strokeStyle = getRGB(0);
u.ctx.fillStyle = getRGB(0);
let deg360 = 2 * Math.PI;
let p = new Path2D();
for (let i = 0; i < d[0].length; i++) {
let xVal = d[0][i];
let yVal = d[1][i];
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
let cx = valToPosX(xVal, scaleX, xDim, xOff);
let cy = valToPosY(yVal, scaleY, yDim, yOff);
p.moveTo(cx + size/2, cy);
arc(p, cx, cy, size/2, 0, deg360);
}
}
u.ctx.fill(p);
});
return null;
};
function render() {
const opts = {
title: "",
mode: 2,
width: width,
height: height,
legend: {
show: false
},
cursor: { drag: { x: false, y: false } },
axes: [
{ label: 'Intensity [FLOPS/Byte]' },
{ label: 'Performace [GFLOPS]' }
],
scales: {
x: {
time: false,
range: [0.01, 1000],
distr: 3, // Render as log
log: 10, // log exp
},
y: {
range: [1.0, nearestThousand(cluster.flopRateSimd.value || maxY)],
distr: 3, // Render as log
log: 10, // log exp
},
},
series: [
{},
{ paths: renderTime ? drawColorPoints : drawPoints }
],
hooks: {
drawClear: [
u => {
u.series.forEach((s, i) => {
if (i > 0)
s._paths = null;
});
},
],
draw: [
u => { // draw roofs when cluster set
// console.log(u)
if (cluster != null) {
const padding = u._padding // [top, right, bottom, left]
u.ctx.strokeStyle = 'black'
u.ctx.lineWidth = 2
u.ctx.beginPath()
const ycut = 0.01 * cluster.memoryBandwidth.value
const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value
const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value
const scalarKneeX = u.valToPos(scalarKnee, 'x', true), // Value, axis, toCanvasPixels
simdKneeX = u.valToPos(simdKnee, 'x', true),
flopRateScalarY = u.valToPos(cluster.flopRateScalar.value, 'y', true),
flopRateSimdY = u.valToPos(cluster.flopRateSimd.value, 'y', true)
if (scalarKneeX < width - padding[1]) { // Top horizontal roofline
u.ctx.moveTo(scalarKneeX, flopRateScalarY)
u.ctx.lineTo(width - padding[1], flopRateScalarY)
}
if (simdKneeX < width - padding[1]) { // Lower horitontal roofline
u.ctx.moveTo(simdKneeX, flopRateSimdY)
u.ctx.lineTo(width - padding[1], flopRateSimdY)
}
let x1 = u.valToPos(0.01, 'x', true),
y1 = u.valToPos(ycut, 'y', true)
let x2 = u.valToPos(simdKnee, 'x', true),
y2 = flopRateSimdY
let xAxisIntersect = lineIntersect(
x1, y1, x2, y2,
u.valToPos(0.01, 'x', true), u.valToPos(1.0, 'y', true), // X-Axis Start Coords
u.valToPos(1000, 'x', true), u.valToPos(1.0, 'y', true) // X-Axis End Coords
)
if (xAxisIntersect.x > x1) {
x1 = xAxisIntersect.x
y1 = xAxisIntersect.y
}
// Diagonal
u.ctx.moveTo(x1, y1)
u.ctx.lineTo(x2, y2)
u.ctx.stroke()
// Reset grid lineWidth
u.ctx.lineWidth = 0.15
}
}
]
},
};
uplot = new uPlot(opts, data, plotWrapper);
}
// Copied from Histogram
onMount(() => {
render()
})
onDestroy(() => {
if (uplot)
uplot.destroy()
if (timeoutId != null)
clearTimeout(timeoutId)
})
function sizeChanged() {
if (timeoutId != null)
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
timeoutId = null
if (uplot)
uplot.destroy()
render()
}, 200)
}
$: sizeChanged(width, height)
</script>
{#if data != null}
<div bind:this={plotWrapper}/>
{:else}
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card>
{/if}

View File

@ -363,3 +363,66 @@ export function binsFromFootprint(weights, scope, values, numBins) {
bins: bins bins: bins
} }
} }
export function transformDataForRoofline(flopsAny, memBw, renderTime) { // Uses Metric Object
const nodes = flopsAny.series.length
const timesteps = flopsAny.series[0].data.length
/* c will contain values from 0 to 1 representing the time */
const x = [], y = [], c = []
if (flopsAny && memBw) {
for (let i = 0; i < nodes; i++) {
const flopsData = flopsAny.series[i].data
const memBwData = memBw.series[i].data
for (let j = 0; j < timesteps; j++) {
const f = flopsData[j], m = memBwData[j]
const intensity = f / m
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
continue
x.push(intensity)
y.push(f)
c.push(renderTime ? j / timesteps : 0)
}
}
} else {
console.warn("transformData: metrics for 'mem_bw' and/or 'flops_any' missing!")
}
return {
x, y, c,
xLabel: 'Intensity [FLOPS/byte]',
yLabel: 'Performance [GFLOPS]'
}
}
// Return something to be plotted. The argument shall be the result of the
// `nodeMetrics` GraphQL query.
export function transformPerNodeDataForRoofline(nodes) {
const x = [], y = [], c = []
for (let node of nodes) {
let flopsAny = node.metrics.find(m => m.name == 'flops_any' && m.scope == 'node')?.metric
let memBw = node.metrics.find(m => m.name == 'mem_bw' && m.scope == 'node')?.metric
if (!flopsAny || !memBw) {
console.warn("transformPerNodeData: metrics for 'mem_bw' and/or 'flops_any' missing!")
continue
}
let flopsData = flopsAny.series[0].data, memBwData = memBw.series[0].data
const f = flopsData[flopsData.length - 1], m = memBwData[flopsData.length - 1]
const intensity = f / m
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
continue
x.push(intensity)
y.push(f)
c.push(0)
}
return {
x, y, c,
xLabel: 'Intensity [FLOPS/byte]',
yLabel: 'Performance [GFLOPS]'
}
}