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
13 changes: 13 additions & 0 deletions src/slurmcostmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ function SuccessFailChart({ data }) {

function Summary({ summary, details, daily, monthly }) {
const sparklineData = daily.map(d => d.core_hours);
const gpuSparklineData = daily.map(d => d.gpu_hours || 0);
const ratio = summary.projected_revenue
? summary.total / summary.projected_revenue
: 1;
Expand Down Expand Up @@ -477,6 +478,12 @@ function Summary({ summary, details, daily, monthly }) {
null,
React.createElement('th', null, 'Total Core Hours'),
React.createElement('td', null, summary.core_hours)
),
React.createElement(
'tr',
null,
React.createElement('th', null, 'Total GPU Hours'),
React.createElement('td', null, summary.gpu_hours || 0)
)
)
)
Expand All @@ -489,6 +496,12 @@ function Summary({ summary, details, daily, monthly }) {
value: summary.core_hours,
renderChart: () => React.createElement(KpiSparkline, { data: sparklineData })
}),
React.createElement(KpiTile, {
label: 'Total GPU-hours',
value: summary.gpu_hours,
renderChart: () =>
React.createElement(KpiSparkline, { data: gpuSparklineData })
}),
React.createElement(KpiTile, {
label: 'Cost recovery ratio',
value: `${(ratio * 100).toFixed(1)}%`,
Expand Down
45 changes: 38 additions & 7 deletions src/slurmdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,14 @@ def aggregate_usage(self, start_time, end_time):
"""Aggregate usage metrics by account and time period."""
rows = self.fetch_usage_records(start_time, end_time)
agg = {}
totals = {'daily': {}, 'monthly': {}, 'yearly': {}}
totals = {
'daily': {},
'monthly': {},
'yearly': {},
'daily_gpu': {},
'monthly_gpu': {},
'yearly_gpu': {},
}
for row in rows:
start = self._to_datetime(row['time_start'])
end = self._to_datetime(row['time_end'] or row['time_start'])
Expand All @@ -271,20 +278,28 @@ def aggregate_usage(self, start_time, end_time):
cpus = float(row.get('cpus_alloc') or 0)
except (TypeError, ValueError):
cpus = 0.0
gpus = self._parse_tres(row.get('tres_alloc'), 'gpu')
if not gpus:
gpus = self._parse_tres(row.get('tres_alloc'), 'gres/gpu')

totals['daily'][day] = totals['daily'].get(day, 0.0) + cpus * dur_hours
totals['monthly'][month] = totals['monthly'].get(month, 0.0) + cpus * dur_hours
totals['yearly'][year] = totals['yearly'].get(year, 0.0) + cpus * dur_hours
totals['daily_gpu'][day] = totals['daily_gpu'].get(day, 0.0) + gpus * dur_hours
totals['monthly_gpu'][month] = totals['monthly_gpu'].get(month, 0.0) + gpus * dur_hours
totals['yearly_gpu'][year] = totals['yearly_gpu'].get(year, 0.0) + gpus * dur_hours

month_entry = agg.setdefault(month, {})
acct_entry = month_entry.setdefault(
account,
{
'core_hours': 0.0,
'gpu_hours': 0.0,
'users': {},
},
)
acct_entry['core_hours'] += cpus * dur_hours
acct_entry['gpu_hours'] += gpus * dur_hours
user_entry = acct_entry['users'].setdefault(
user, {'core_hours': 0.0, 'jobs': {}}
)
Expand Down Expand Up @@ -345,6 +360,7 @@ def export_summary(self, start_time, end_time):
'invoices': [],
}
total_ch = 0.0
total_gpu = 0.0
total_cost = 0.0

rates_path = os.path.join(os.path.dirname(__file__), 'rates.json')
Expand Down Expand Up @@ -407,11 +423,13 @@ def export_summary(self, start_time, end_time):
{
'account': account,
'core_hours': round(vals['core_hours'], 2),
'gpu_hours': round(vals.get('gpu_hours', 0.0), 2),
'cost': round(acct_cost, 2),
'users': users,
}
)
total_ch += vals['core_hours']
total_gpu += vals.get('gpu_hours', 0.0)
total_cost += acct_cost
start_dt = (
datetime.fromisoformat(start_time)
Expand All @@ -427,18 +445,31 @@ def export_summary(self, start_time, end_time):
'period': f"{start_dt.strftime('%Y-%m-%d')} to {end_dt.strftime('%Y-%m-%d')}",
'total': round(total_cost, 2),
'core_hours': round(total_ch, 2),
'gpu_hours': round(total_gpu, 2),
}
summary['daily'] = [
{'date': d, 'core_hours': round(v, 2)}
for d, v in sorted(totals['daily'].items())
{
'date': d,
'core_hours': round(totals['daily'].get(d, 0.0), 2),
'gpu_hours': round(totals.get('daily_gpu', {}).get(d, 0.0), 2),
}
for d in sorted(set(totals['daily']) | set(totals.get('daily_gpu', {})))
]
summary['monthly'] = [
{'month': m, 'core_hours': round(v, 2)}
for m, v in sorted(totals['monthly'].items())
{
'month': m,
'core_hours': round(totals['monthly'].get(m, 0.0), 2),
'gpu_hours': round(totals.get('monthly_gpu', {}).get(m, 0.0), 2),
}
for m in sorted(set(totals['monthly']) | set(totals.get('monthly_gpu', {})))
]
summary['yearly'] = [
{'year': y, 'core_hours': round(v, 2)}
for y, v in sorted(totals['yearly'].items())
{
'year': y,
'core_hours': round(totals['yearly'].get(y, 0.0), 2),
'gpu_hours': round(totals.get('yearly_gpu', {}).get(y, 0.0), 2),
}
for y in sorted(set(totals['yearly']) | set(totals.get('yearly_gpu', {})))
]
summary['invoices'] = self.fetch_invoices(start_time, end_time)
return summary
Expand Down
12 changes: 11 additions & 1 deletion test/unit/billing_summary.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def test_export_summary_aggregates_costs(self):
'2023-10': {
'acct': {
'core_hours': 10.0,
'gpu_hours': 5.0,
'users': {
'user1': {'core_hours': 10.0, 'jobs': {}}
},
Expand All @@ -20,15 +21,24 @@ def test_export_summary_aggregates_costs(self):
with mock.patch.object(
SlurmDB,
'aggregate_usage',
return_value=(usage, {'daily': {}, 'monthly': {}, 'yearly': {}}),
return_value=(usage, {
'daily': {},
'monthly': {},
'yearly': {},
'daily_gpu': {},
'monthly_gpu': {},
'yearly_gpu': {},
}),
):
with mock.patch.object(SlurmDB, 'fetch_invoices', return_value=invoices):
db = SlurmDB()
summary = db.export_summary('2023-10-01', '2023-10-31')
self.assertEqual(summary['summary']['total'], 0.2)
self.assertEqual(summary['details'][0]['account'], 'acct')
self.assertEqual(summary['details'][0]['core_hours'], 10.0)
self.assertEqual(summary['details'][0]['gpu_hours'], 5.0)
self.assertEqual(summary['details'][0]['cost'], 0.2)
self.assertEqual(summary['summary']['gpu_hours'], 5.0)
self.assertEqual(summary['invoices'][0]['file'], 'inv1.pdf')

def test_export_summary_applies_overrides_and_discounts(self):
Expand Down
Loading