Skip to content

Commit 1a61dad

Browse files
committed
start looking at memory
1 parent 2cc33a9 commit 1a61dad

17 files changed

Lines changed: 1049 additions & 210 deletions

‎include/utils/matrix.h‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,17 @@ typedef void (*matrix_broadcast_fill_values_fn)(matrix *A, broadcast_type type,
106106
typedef matrix *(*matrix_diag_vec_alloc_fn)(matrix *A);
107107
typedef void (*matrix_diag_vec_fill_values_fn)(matrix *A, matrix *out);
108108

109+
/* sum_all_rows: C = sum over all rows of A. Output shape is (1, A->n).
110+
Implementations choose the output's matrix type to preserve structure
111+
(sparse → sparse, permuted_dense → permuted_dense, stacked_pd →
112+
permuted_dense over the union of block col_perms). idx_map (sized at
113+
A->nnz, allocated by caller) is filled such that idx_map[k] is the
114+
position in C->x where A's k-th cell (in A->base.x ordering)
115+
accumulates — eval consumers run `accumulator(A->x, A->nnz, idx_map,
116+
C->x)` unchanged. No paired _fill_values is needed: sum's existing
117+
eval_jacobian does the value fill polymorphically. */
118+
typedef matrix *(*matrix_sum_all_rows_alloc_fn)(matrix *A, int *idx_map);
119+
109120
typedef void (*matrix_free_fn)(matrix *self);
110121

111122
struct matrix
@@ -141,6 +152,7 @@ struct matrix
141152
matrix_broadcast_fill_values_fn broadcast_fill_values;
142153
matrix_diag_vec_alloc_fn diag_vec_alloc;
143154
matrix_diag_vec_fill_values_fn diag_vec_fill_values;
155+
matrix_sum_all_rows_alloc_fn sum_all_rows_alloc;
144156

145157
/* Lifecycle */
146158
matrix_free_fn free_fn;

‎include/utils/permuted_dense.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ matrix *new_permuted_dense(int m, int n, int m0, int n0, const int *row_perm,
8181
col_perm = [0..n-1], dense block fills the full (m, n) shape. */
8282
matrix *new_permuted_dense_full(int m, int n, const double *data);
8383

84+
/* Like new_permuted_dense, but the X buffer is borrowed from the caller
85+
(owns_X = false). The caller is responsible for keeping borrowed_X alive
86+
for at least the lifetime of the returned PD and for freeing it
87+
separately. row_perm / col_perm / row_inv / col_inv are still owned by
88+
the PD (copied / computed internally). Used by stacked_pd output blocks
89+
that share one shared values buffer. */
90+
matrix *new_permuted_dense_view(int m, int n, int m0, int n0, const int *row_perm,
91+
const int *col_perm, double *borrowed_X);
92+
8493
/* Ensure A->kernel_dwork is sized at least 'size' doubles. Grows in
8594
place; contents are NOT preserved. */
8695
void permuted_dense_ensure_kernel_dwork(const permuted_dense *A, size_t size);

‎include/utils/stacked_pd.h‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,35 @@
2222
#include "matrix.h"
2323
#include "permuted_dense.h"
2424

25+
/* Shape metadata for one would-be source block of a coalesce. Used by
26+
spd_blockwise_alloc_coalesce to drive symbolic coalesce without
27+
allocating per-block Gram PDs, and by spd_blockwise_fill_coalesce to
28+
re-aim a shared scratch PD at each input block's metadata.
29+
- (m0, n0): block shape.
30+
- row_perm, col_perm: aliased pointers into operand arrays; not owned.
31+
- scratch_*_needed: sizes the matching fill op will read from its
32+
destination PD's kernel_dwork / kernel_iwork. Coalesce ignores these;
33+
spd_blockwise uses them to size the shared scratch PD once at the
34+
max-across-blocks. */
35+
typedef struct
36+
{
37+
int m0;
38+
int n0;
39+
/* Both arrays are SP_MALLOC'd by the shape callback and owned by the
40+
per_block_shapes array (freed by stacked_pd_free). */
41+
int *row_perm;
42+
int *col_perm;
43+
/* Sizes the matching fill op will read from its destination PD's
44+
kernel_dwork / kernel_iwork. */
45+
size_t scratch_dwork_needed;
46+
size_t scratch_iwork_needed;
47+
/* Optional metadata to seed scratch->kernel_iwork[0..init_len) with
48+
before invoking the per-block fill op. Used by BTA_pd_spd's fill
49+
which reads s_max / max_n0_A from iwork[0..1]. */
50+
int iwork_init[2];
51+
int iwork_init_len;
52+
} spd_per_block_shape;
53+
2554
/* stacked_pd represents a matrix that is the (vertical) union of 'n_blocks'
2655
permuted_dense blocks. Two different blocks have disjoint row permutations,
2756
but the column permutations may overlap across blocks.
@@ -56,6 +85,44 @@ typedef struct stacked_pd
5685
the kron-expanded transpose-of-A buffer across fills. NULL when
5786
not used. */
5887
permuted_dense *kernel_pd_scratch;
88+
89+
/* Streaming-fill state for stacked_pds produced by
90+
spd_blockwise_alloc_coalesce. NULL when not used. All buffers are
91+
sized at alloc time so the matching fill function does zero
92+
SP_MALLOC.
93+
- scratch_X: per-iteration buffer holding one source block's Gram.
94+
Sized at max over input blocks of (m0_partial * n0_partial).
95+
- scratch_dwork / scratch_iwork: workspaces the per-block fill op
96+
reads from its (scratch) destination PD's kernel_dwork /
97+
kernel_iwork. Sized at max-across-blocks.
98+
- per_block_shapes: shape (and scratch sizes) for each input block's
99+
partial. Indexed [0, n_input_blocks). The scratch PD's metadata
100+
(m0, n0, row_perm, col_perm) is re-aimed from this array each
101+
iteration.
102+
- n_input_blocks: number of input blocks (length of per_block_shapes
103+
and of src_to_outs_p[]-1). Distinct from n_blocks (which counts
104+
OUTPUT blocks after coalesce).
105+
- src_to_outs_p, src_to_outs_data: CSR-style inverse of
106+
src_block_idx_p/_data. src_to_outs_data[src_to_outs_p[k] ..
107+
src_to_outs_p[k+1]) lists the output block indices that block k
108+
contributes to. Drives the per-input-block scatter loop in fill. */
109+
double *scratch_X;
110+
size_t scratch_X_capacity;
111+
double *scratch_dwork;
112+
size_t scratch_dwork_capacity;
113+
int *scratch_iwork;
114+
size_t scratch_iwork_capacity;
115+
/* Re-initialized per iteration from per_block_shapes[k].row_perm /
116+
col_perm. Sized at (m, n) — the global dimensions. Some fill ops
117+
(e.g. BTA_pd_spd_fill_values at stacked_pd_linalg.c:477) read
118+
C->col_inv to look up scatter positions; the scratch must have
119+
it populated. */
120+
int *scratch_row_inv;
121+
int *scratch_col_inv;
122+
spd_per_block_shape *per_block_shapes;
123+
int n_input_blocks;
124+
int *src_to_outs_p;
125+
int *src_to_outs_data;
59126
} stacked_pd;
60127

61128
/* Constructor for stacked_pd. Takes ownership of every block in 'blocks'. The
@@ -75,6 +142,26 @@ matrix *new_stacked_pd_unchecked(int m, int n, int n_blocks, permuted_dense **bl
75142
const int *src_block_idx_p,
76143
const int *src_block_idx);
77144

145+
/* Like new_stacked_pd_unchecked, but the blocks' X pointers already point
146+
into the caller-supplied `shared_x` buffer at the correct offsets
147+
(typically constructed via new_permuted_dense_view). No absorb / memcpy
148+
is performed. The stacked_pd takes ownership of `shared_x` and frees it
149+
on destruction; each block must have owns_X == false. */
150+
matrix *new_stacked_pd_borrowed_x(int m, int n, int n_blocks,
151+
permuted_dense **blocks,
152+
const int *src_block_idx_p,
153+
const int *src_block_idx, double *shared_x);
154+
155+
/* Build a stacked_pd from precomputed per-block shapes. Sums total_nnz,
156+
SP_MALLOCs one shared X buffer, constructs n_blocks view PDs pointing
157+
into that buffer at sequential offsets, and wraps via
158+
new_stacked_pd_borrowed_x. No absorb / double-count. Identity
159+
src_block_idx (one source per output block). Reads only the shape's
160+
m0 / n0 / row_perm / col_perm fields; per-block kernel_iwork /
161+
kernel_dwork are the caller's responsibility. */
162+
matrix *new_stacked_pd_from_shapes_unchecked(int m, int n, int n_blocks,
163+
const spd_per_block_shape *shapes);
164+
78165
/* Filter-map over a stacked_pd's blocks. For each block of B, calls op(Bk,
79166
ctx). Drop blocks whose result has nnz == 0 and assembles the survivors into
80167
a new stacked_pd (dimensions Cm x Cn) with one source per output block. */
@@ -93,13 +180,31 @@ matrix *coalesce_spd_alloc(const stacked_pd *A);
93180
overlapping row permutations. */
94181
matrix *coalesce_spd_alloc_unchecked(const stacked_pd *A);
95182

183+
/* Symbolic coalesce driven by a shapes array (no per-block X / PDs needed).
184+
Returns an output stacked_pd whose blocks are views into one shared X
185+
buffer that the output owns. The output's src_block_idx_p / src_block_idx
186+
hold the forward map; callers that need the inverse read those fields
187+
directly. Equivalent to coalesce_spd_alloc_unchecked but works from shape
188+
metadata only — used by spd_blockwise_alloc_coalesce to skip the
189+
per-block Gram materialization. */
190+
matrix *coalesce_spd_alloc_from_shapes_unchecked(const spd_per_block_shape *shapes,
191+
int n_input_blocks, int m, int n);
192+
96193
/* Fill values of C = coalesce(A). */
97194
void coalesce_spd_fill_values(const stacked_pd *A, stacked_pd *C);
98195

99196
/* Same scatter as coalesce_spd_fill_values but with += instead of =. Caller is
100197
responsible for zeroing C->base.x first. */
101198
void coalesce_spd_fill_values_accumulate(const stacked_pd *A, stacked_pd *C);
102199

200+
/* Scatter the cells of one source PD into one output PD (using out_k's
201+
row_inv / col_inv to map global row / col indices to local positions),
202+
accumulating with += (caller must zero out_k->X before the first call
203+
in a scatter cycle). Used by the per-input-block streaming fill in
204+
spd_blockwise_fill_coalesce_accumulate. */
205+
void scatter_one_source_into_one_output_accumulate(const permuted_dense *src_k,
206+
permuted_dense *out_k);
207+
103208
/* Re-index `idx_map` in place from CSR ordering to spd-native ordering
104209
(block-major). Useful for atoms that build idx_maps from a `to_csr`
105210
view at init time and want to read spd values directly at eval time,

‎include/utils/tracked_alloc.h‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,61 @@
2121
#include <stddef.h>
2222
#include <stdlib.h>
2323

24-
extern size_t g_allocated_bytes;
24+
/* Platform shim for "how many usable bytes are at this malloc'd pointer".
25+
Apple's malloc_size, glibc's malloc_usable_size, MSVC's _msize. The
26+
returned size is the *usable* size (may exceed the requested size by a
27+
few bytes of allocator rounding), but alloc and free use the same
28+
function so the running totals stay symmetric. */
29+
#if defined(__APPLE__)
30+
#include <malloc/malloc.h>
31+
#define TRACKED_BLOCK_SIZE(p) malloc_size(p)
32+
#elif defined(_WIN32) || defined(_WIN64)
33+
#include <malloc.h>
34+
#define TRACKED_BLOCK_SIZE(p) _msize(p)
35+
#else
36+
#include <malloc.h>
37+
#define TRACKED_BLOCK_SIZE(p) malloc_usable_size(p)
38+
#endif
39+
40+
extern size_t g_allocated_bytes; /* current live bytes */
41+
extern size_t g_peak_bytes; /* high-water mark since last reset */
2542

2643
static inline void *SP_MALLOC(size_t size)
2744
{
2845
void *ptr = malloc(size);
29-
if (ptr) g_allocated_bytes += size;
46+
if (ptr)
47+
{
48+
g_allocated_bytes += TRACKED_BLOCK_SIZE(ptr);
49+
if (g_allocated_bytes > g_peak_bytes) g_peak_bytes = g_allocated_bytes;
50+
}
3051
return ptr;
3152
}
3253

3354
static inline void *SP_CALLOC(size_t count, size_t size)
3455
{
3556
void *ptr = calloc(count, size);
36-
if (ptr) g_allocated_bytes += count * size;
57+
if (ptr)
58+
{
59+
g_allocated_bytes += TRACKED_BLOCK_SIZE(ptr);
60+
if (g_allocated_bytes > g_peak_bytes) g_peak_bytes = g_allocated_bytes;
61+
}
3762
return ptr;
3863
}
3964

65+
static inline void SP_FREE(void *ptr)
66+
{
67+
if (ptr)
68+
{
69+
g_allocated_bytes -= TRACKED_BLOCK_SIZE(ptr);
70+
free(ptr);
71+
}
72+
}
73+
74+
/* Auto-route plain malloc/calloc/free in caller translation units through
75+
the tracked wrappers. Defined AFTER the wrapper bodies so SP_MALLOC /
76+
SP_CALLOC / SP_FREE themselves still see the real stdlib symbols. */
77+
#define malloc SP_MALLOC
78+
#define calloc SP_CALLOC
79+
#define free SP_FREE
80+
4081
#endif /* TRACKED_ALLOC_H */

‎src/atoms/affine/sum.c‎

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -89,43 +89,57 @@ static void jacobian_init_impl(expr *node)
8989

9090
/* initialize child's jacobian */
9191
jacobian_init(x);
92-
CSR_matrix *Jx = x->jacobian->to_csr(x->jacobian);
9392

94-
/* we never have to store more than the child's nnz */
95-
CSR_matrix *jac = new_CSR_matrix(node->size, node->n_vars, Jx->nnz);
96-
node->work->iwork = SP_MALLOC(MAX(jac->n, Jx->nnz) * sizeof(int));
97-
snode->idx_map = SP_MALLOC(Jx->nnz * sizeof(int));
98-
99-
/* the idx_map array maps each nonzero entry j in x->jacobian
100-
to the corresponding index in the output row matrix C. Specifically, for
101-
each nonzero entry j in A, idx_map[j] gives the position in C->x where
102-
the value from x->jacobian->x[j] should be accumulated. */
93+
/* idx_map maps each nonzero entry j of x->jacobian (in its native
94+
base.x order) to the corresponding index in this node's jacobian
95+
value array. eval_jacobian then runs
96+
accumulator(x->jacobian->x, x->jacobian->nnz, idx_map, ...). */
97+
snode->idx_map = SP_MALLOC(x->jacobian->nnz * sizeof(int));
10398

10499
if (axis == -1)
105100
{
106-
sum_all_rows_csr_alloc(Jx, jac, node->work->iwork, snode->idx_map);
107-
}
108-
else if (axis == 0)
109-
{
110-
sum_block_of_rows_csr_alloc(Jx, jac, x->d1, node->work->iwork,
111-
snode->idx_map);
101+
/* Polymorphic native-layout sum: walks x->jacobian without going
102+
through CSR. The output's matrix type follows the input's
103+
(sparse → sparse, PD → PD, stacked_pd → PD over col union).
104+
idx_map comes back in x->jacobian->base.x ordering, so no CSR
105+
re-permutation is needed. */
106+
node->jacobian =
107+
x->jacobian->sum_all_rows_alloc(x->jacobian, snode->idx_map);
112108
}
113-
else if (axis == 1)
109+
else
114110
{
115-
sum_evenly_spaced_rows_csr_alloc(Jx, jac, node->size, node->work->iwork,
116-
snode->idx_map);
117-
}
111+
/* axis=0 / axis=1: still go through CSR. The output has multiple
112+
rows and the natural per-type output is less clean; the CSR
113+
path is fine since these aren't hot in current benchmarks. */
114+
CSR_matrix *Jx = x->jacobian->to_csr(x->jacobian);
115+
size_t max_out_nnz =
116+
MIN((size_t) Jx->nnz, (size_t) node->size * (size_t) node->n_vars);
117+
CSR_matrix *jac =
118+
new_CSR_matrix(node->size, node->n_vars, (int) max_out_nnz);
119+
node->work->iwork = SP_MALLOC(MAX(jac->n, Jx->nnz) * sizeof(int));
120+
121+
if (axis == 0)
122+
{
123+
sum_block_of_rows_csr_alloc(Jx, jac, x->d1, node->work->iwork,
124+
snode->idx_map);
125+
}
126+
else
127+
{
128+
sum_evenly_spaced_rows_csr_alloc(Jx, jac, node->size, node->work->iwork,
129+
snode->idx_map);
130+
}
118131

119-
/* For stacked_pd children, child->jacobian->base.x is block-major while
120-
csr->x is row-major sorted. Re-index idx_map so it can be applied
121-
directly to base.x in eval_jacobian. */
122-
if (x->jacobian->is_stacked_pd)
123-
{
124-
compose_csr_idx_map_for_spd((const stacked_pd *) x->jacobian, Jx,
125-
snode->idx_map);
126-
}
132+
/* For stacked_pd children, child->jacobian->base.x is block-major
133+
while csr->x is row-major sorted. Re-index idx_map so it can be
134+
applied directly to base.x in eval_jacobian. */
135+
if (x->jacobian->is_stacked_pd)
136+
{
137+
compose_csr_idx_map_for_spd((const stacked_pd *) x->jacobian, Jx,
138+
snode->idx_map);
139+
}
127140

128-
node->jacobian = new_sparse_matrix(jac);
141+
node->jacobian = new_sparse_matrix(jac);
142+
}
129143
}
130144

131145
static void eval_jacobian(expr *node)

‎src/problem.c‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ static void problem_lagrange_hess_fill_sparsity(problem *prob, int *iwork);
3232
problem *new_problem(expr *objective, expr **constraints, int n_constraints,
3333
bool verbose)
3434
{
35-
g_allocated_bytes = 0;
35+
/* Baseline the peak watermark to the current live bytes — peak then
36+
captures any growth during this problem's lifetime. Don't reset
37+
g_allocated_bytes itself: allocations made before new_problem are
38+
still alive, and their frees would subtract from this counter. */
39+
g_peak_bytes = g_allocated_bytes;
3640
problem *prob = (problem *) SP_CALLOC(1, sizeof(problem));
3741
if (!prob) return NULL;
3842

@@ -321,7 +325,7 @@ static inline void print_end_message(const Diff_engine_stats *stats)
321325
printf(" Lagrange Hessian (nnz): %d\n", stats->nnz_hessian);
322326
char mem_buf[64];
323327
format_memory(stats->memory_bytes, mem_buf, sizeof(mem_buf));
324-
printf(" Allocated memory: %s\n", mem_buf);
328+
printf(" Peak memory: %s\n", mem_buf);
325329

326330
printf("\nTiming (seconds):\n");
327331
printf(" Derivative structure (sparsity): %8.3f\n",
@@ -349,7 +353,7 @@ void free_problem(problem *prob)
349353

350354
if (prob->verbose)
351355
{
352-
prob->stats.memory_bytes = g_allocated_bytes;
356+
prob->stats.memory_bytes = g_peak_bytes;
353357
print_end_message(&prob->stats);
354358
}
355359

0 commit comments

Comments
 (0)