Skip to content
Merged

Migrate #1030

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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ MDX is GitHub-flavored markdown plus components, so:
- **Table of contents.** Every page gets an "On this page" list of its top two heading levels beside the body, generated from the headings. Nothing to write or keep in sync.
- **Code blocks** use normal fences and are syntax highlighted. A `condor` fence is highlighted as a submit file. Wrap a fence in `<Terminal>` for a terminal session (dark) or `<File>` for a file's contents (blue rule); plain fences are for everything else.
- **Callouts** are `<Tip header="Note about NetIDs">…</Tip>`; leave out `header` for a plain box.
- **Other blocks**: `<Button href="…">` inside `<ButtonRow>`, `<Columns>` of `<Column>` (`accent` for the red rule, `width="30%"` for a fixed share), `<Cards>` of `<Card icon="…" title="…">`, `<MiniBar>` around a heading, `<CommandTable>` around a command table, `<Footnote>` under a table. Data-driven pieces are `<GpuTable owner="campus" />`, `<UpcomingEvents tag="…" limit={2} />`, `<ExcerptTabs flag="software_icon" more />` and `<FacilitatorGrid />`. The standard set is the map in `app/uw-research-computing/_components/docComponents.tsx`.
- **Other blocks**: `<Button href="…">` inside `<ButtonRow>`, `<Columns>` of `<Column>` (`accent` for the red rule, `width="30%"` for a fixed share), `<Cards>` of `<Card icon="…" title="…">`, `<MiniBar>` around a heading, `<CommandTable>` around a command table, `<Footnote>` under a table. Data-driven pieces are `<GpuTable />`, `<UpcomingEvents tag="…" limit={2} />`, `<ExcerptTabs flag="software_icon" more />` and `<FacilitatorGrid />`. The standard set is the map in `app/uw-research-computing/_components/docComponents.tsx`.
- **Leave a blank line** after an opening component tag and before its closing tag; otherwise the markdown inside is treated as inline text.
- **Raw HTML** (`<details>`, `<iframe>`, `<img>`) works but must be valid JSX: `className` not `class`, `style={{ maxWidth: '60%' }}` not a string, `<br />` and `<img … />` self-closed, `{/* comments */}` not `<!-- -->`. A `<` or `{` in prose that is not a tag must be escaped: `\<1 GB`, `\{`.
- **Tables** need a header row and a `|---|---|` separator line. HTML tables with multi-line cells do not compile; use a pipe table with `<br />` instead.
Expand Down
221 changes: 172 additions & 49 deletions app/uw-research-computing/_components/GpuTable.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,185 @@
import type { ReactNode } from 'react';
import { Box } from '@mui/material';
import { readYaml } from '../_lib/docs';
import { Footnote } from './DocBlocks';

interface Gpu {
number_of_servers?: number | string;
'number of servers'?: number | string;
names?: string;
gpus_per_server?: number | string;
gpu_type?: string;
hardware_generation?: number | string;
gpu_name: string;
gpu_type: string;
hardware_generation: number | string;
gpu_memory?: string;
owner?: string;
gpus_minimum_memory: string;
shared_memory?: boolean;
open_gpus: number;
backfill_gpus: number;
open_servers?: string[];
}

/** Right-aligned, fixed-width digits so the counts line up. */
const NUM_SX = { textAlign: 'right', whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' } as const;
const HEAD_SX = { verticalAlign: 'bottom' } as const;
const TOTAL_SX = { fontWeight: 700, borderTop: 2 } as const;

const Num = ({ children, sx }: { children: ReactNode; sx?: object }) => (
<Box component='td' sx={{ ...NUM_SX, ...sx }}>
{children}
</Box>
);

/** YAML reads `6.0` as the number 6; compute capabilities print as x.y. */
const capability = (gpu: Gpu) =>
typeof gpu.hardware_generation === 'number' ? gpu.hardware_generation.toFixed(1) : gpu.hardware_generation;

/** The GPU's name with the marks that point at the notes under the table. */
const GpuName = ({ gpu }: { gpu: Gpu }) => (
<td>
{gpu.gpu_name}
{gpu.shared_memory && <sup>§</sup>}
{gpu.gpu_type.includes('MIG') && <sup>‡</sup>}
</td>
);

const sum = (gpus: Gpu[], count: (gpu: Gpu) => number) => gpus.reduce((total, gpu) => total + count(gpu), 0);

/**
* The GPU Lab hardware table in the GPU jobs guide, generated from
* `content/uw-research-computing/data/gpus.yaml` (legacy `_data/gpus.yaml`)
* as the Liquid loop did: only the servers with the given owner.
* The GPUs in the HTC pool, from `content/uw-research-computing/data/gpus.yaml`:
* the types with open capacity, with their open, backfill and total counts,
* then the types only reachable as backfill in a collapsed table, then the
* notes the marks in both refer to.
*/
const GpuTable = ({ owner = 'campus' }: { owner?: string }) => {
const gpus = readYaml<Gpu[]>('data/gpus.yaml').filter((gpu) => gpu.owner === owner);
const GpuTable = () => {
const gpus = readYaml<Gpu[]>('data/gpus.yaml');
const open = gpus.filter((gpu) => gpu.open_gpus > 0);
const backfillOnly = gpus.filter((gpu) => gpu.open_gpus === 0);
const openTotal = sum(gpus, (gpu) => gpu.open_gpus);
const backfillTotal = sum(gpus, (gpu) => gpu.backfill_gpus);
const anySharedMemory = gpus.some((gpu) => gpu.shared_memory);

return (
<div className='md-table-scroll'>
<table className='gtable'>
<thead>
<tr>
<th>Number of Servers</th>
<th>Names</th>
<th>GPUs / Server</th>
<th>
GPU Type (<code>DeviceName</code>)
</th>
<th>
Hardware Generation <code>Capability</code>
</th>
<th>
GPU Memory <code>GlobalMemoryMB</code>
</th>
</tr>
</thead>
<tbody>
{gpus.map((gpu, index) => (
<tr key={`${gpu.names}-${index}`}>
{/* One entry in the data is keyed `number of servers` (Liquid printed it blank). */}
<td>{gpu.number_of_servers ?? gpu['number of servers']}</td>
<td>{gpu.names}</td>
<td>{gpu.gpus_per_server}</td>
<td>{gpu.gpu_type}</td>
{/* YAML reads `6.0` as the number 6; compute capabilities print as x.y. */}
<td>
{typeof gpu.hardware_generation === 'number'
? gpu.hardware_generation.toFixed(1)
: gpu.hardware_generation}
</td>
<td>{gpu.gpu_memory}</td>
<>
<Box sx={{ overflowX: 'auto', mb: 2 }}>
<table>
<thead>
<tr>
<Box component='th' rowSpan={2} sx={HEAD_SX}>
GPU Name
</Box>
<Box component='th' rowSpan={2} sx={{ ...HEAD_SX, ...NUM_SX }}>
Hardware Generation
<br />
<small>
(<code>GPUs_Capability</code>)
</small>
</Box>
<Box component='th' rowSpan={2} sx={{ ...HEAD_SX, ...NUM_SX }}>
GPU Memory (VRAM)
<br />
<small>
(<code>gpus_minimum_memory</code>)<sup>†</sup>
</small>
</Box>
<Box component='th' colSpan={3} sx={{ textAlign: 'center' }}>
Number of GPUs
</Box>
</tr>
<tr>
<Box component='th' sx={NUM_SX}>
Open capacity
</Box>
<Box component='th' sx={NUM_SX}>
Backfill
</Box>
<Box component='th' sx={NUM_SX}>
Total
</Box>
</tr>
</thead>
<tbody>
{open.map((gpu) => (
<tr key={gpu.gpu_name}>
<GpuName gpu={gpu} />
<Num>{capability(gpu)}</Num>
<Num>
<code>{gpu.gpus_minimum_memory}</code>
</Num>
<Num>{gpu.open_gpus}</Num>
<Num>{gpu.backfill_gpus}</Num>
<Num>{gpu.open_gpus + gpu.backfill_gpus}</Num>
</tr>
))}
</tbody>
<tfoot>
<tr>
<Box component='td' colSpan={3} sx={TOTAL_SX}>
Total, all GPU types
</Box>
<Num sx={TOTAL_SX}>{openTotal}</Num>
<Num sx={TOTAL_SX}>{backfillTotal}</Num>
<Num sx={TOTAL_SX}>{openTotal + backfillTotal}</Num>
</tr>
))}
</tbody>
</table>
</div>
</tfoot>
</table>
</Box>

{backfillOnly.length > 0 && (
<details>
<summary>GPU types available only through backfill</summary>
<Box sx={{ overflowX: 'auto' }}>
<table>
<thead>
<tr>
<th>GPU Name</th>
<Box component='th' sx={NUM_SX}>
Capability
</Box>
<Box component='th' sx={NUM_SX}>
Memory<sup>†</sup>
</Box>
<Box component='th' sx={NUM_SX}>
GPUs
</Box>
</tr>
</thead>
<tbody>
{backfillOnly.map((gpu) => (
<tr key={gpu.gpu_name}>
<GpuName gpu={gpu} />
<Num>{capability(gpu)}</Num>
<Num>
<code>{gpu.gpus_minimum_memory}</code>
</Num>
<Num>{gpu.backfill_gpus}</Num>
</tr>
))}
</tbody>
<tfoot>
<tr>
<Box component='td' colSpan={3} sx={TOTAL_SX}>
Total
</Box>
<Num sx={TOTAL_SX}>{sum(backfillOnly, (gpu) => gpu.backfill_gpus)}</Num>
</tr>
</tfoot>
</table>
</Box>
</details>
)}

<Footnote>
<sup>†</sup> The largest <code>gpus_minimum_memory</code> value that still matches this GPU. A higher
value excludes it. A lower value also matches GPUs with less memory.
{anySharedMemory && (
<>
<br />
<sup>§</sup> The CPU and GPU share one pool of memory. Memory your job uses on the CPU leaves less
for the GPU.
</>
)}
<br />
<sup>‡</sup> A MIG slice is part of one A100 GPU. It has 40&nbsp;GB of memory but about 40% of the
computing power of a full A100. Two slices share one A100.
</Footnote>
</>
);
};

Expand Down
Loading
Loading