99#pragma once
1010
1111#include < algorithm>
12+ #include < optional>
1213#include < stdexcept>
1314#include < vector>
1415
@@ -25,38 +26,38 @@ namespace mlx {
2526// where in them a step lands.
2627class Pool {
2728 public:
28- // initial_slots above max_slots is clamped, not rejected: the config default
29- // exceeds the cap of any smaller cache, so this is the normal path.
29+ // Holds its geometry but allocates nothing: every array belongs to the
30+ // stream its work runs on, and the first stream reaches the pool with the
31+ // first write. initial_slots above max_slots is clamped, not rejected: the
32+ // config default exceeds the cap of any smaller cache.
3033 Pool (int initial_slots, int max_slots, int H, int D, ::mlx::core::Dtype dtype)
3134 : dtype_(dtype),
3235 max_slots_ (max_slots),
33- buf_(::mlx::core::zeros(
34- ::mlx::core::Shape{ 1 , H, std::min (initial_slots, max_slots), D} ,
35- dtype) ) {}
36+ initial_slots_(std::min(initial_slots, max_slots)),
37+ H_(H) ,
38+ D_(D ) {}
3639
3740 // Place `update` at slot `start`, casting to the storage dtype if it differs.
3841 void write (int start, int len, const Tensor& update, StreamOrDevice s) {
39- const int H = static_cast <int >(buf_.shape (1 ));
40- const int D = static_cast <int >(buf_.shape (3 ));
4142 if (start < 0 || start + len > max_slots_) {
4243 throw std::runtime_error (" Pool::write: run out of bounds" );
4344 }
4445 if (static_cast <int >(update.shape (2 )) != len) {
4546 throw std::runtime_error (" Pool::write: update length != run length" );
4647 }
47- if (static_cast <int >(update.shape (1 )) != H ||
48- static_cast <int >(update.shape (3 )) != D ) {
48+ if (static_cast <int >(update.shape (1 )) != H_ ||
49+ static_cast <int >(update.shape (3 )) != D_ ) {
4950 throw std::runtime_error (" Pool::write: K/V heads/dim mismatch" );
5051 }
5152 maybe_grow (start + len, s);
5253 const Tensor u = update.dtype () == dtype_
5354 ? update
5455 : ::mlx::core::astype (update, dtype_, s);
5556 buf_ = ::mlx::core::slice_update (
56- buf_,
57+ * buf_,
5758 u,
5859 ::mlx::core::Shape{0 , 0 , start, 0 },
59- ::mlx::core::Shape{1 , H , start + len, D },
60+ ::mlx::core::Shape{1 , H_ , start + len, D_ },
6061 s);
6162 }
6263
@@ -67,15 +68,13 @@ class Pool {
6768 const std::vector<int32_t >& cells,
6869 const Tensor& update,
6970 StreamOrDevice s) {
70- const int H = static_cast <int >(buf_.shape (1 ));
71- const int D = static_cast <int >(buf_.shape (3 ));
7271 const int T = static_cast <int >(cells.size ());
7372 if (static_cast <int >(update.shape (2 )) != T) {
7473 throw std::runtime_error (
7574 " Pool::write_cells: update length != cell count" );
7675 }
77- if (static_cast <int >(update.shape (1 )) != H ||
78- static_cast <int >(update.shape (3 )) != D ) {
76+ if (static_cast <int >(update.shape (1 )) != H_ ||
77+ static_cast <int >(update.shape (3 )) != D_ ) {
7978 throw std::runtime_error (" Pool::write_cells: K/V heads/dim mismatch" );
8079 }
8180 int high = 0 ;
@@ -92,7 +91,7 @@ class Pool {
9291 // put_along_axis broadcasts the one index per token across heads and
9392 // head_dim.
9493 buf_ = ::mlx::core::put_along_axis (
95- buf_,
94+ * buf_,
9695 ::mlx::core::array (
9796 cells.data(), ::mlx::core::Shape{1 , 1 , T, 1 }, ::mlx::core::int32),
9897 u,
@@ -103,29 +102,33 @@ class Pool {
103102 // Slots [start, start+len). A ring read starts mid-pool, so the start matters
104103 // here as much as it does for a write.
105104 Tensor read (int start, int len, StreamOrDevice s) const {
106- const int H = static_cast <int >(buf_.shape (1 ));
107- const int D = static_cast <int >(buf_.shape (3 ));
108- if (start < 0 || start + len > slots ()) {
105+ if (!buf_ || start < 0 || start + len > slots ()) {
109106 throw std::runtime_error (" Pool::read: run out of bounds" );
110107 }
111108 return ::mlx::core::slice (
112- buf_,
109+ * buf_,
113110 ::mlx::core::Shape{0 , 0 , start, 0 },
114- ::mlx::core::Shape{1 , H , start + len, D },
111+ ::mlx::core::Shape{1 , H_ , start + len, D_ },
115112 ::mlx::core::Shape{1 , 1 , 1 , 1 },
116113 s);
117114 }
118115
119- // Slots currently allocated; grows toward max_slots on demand.
116+ // Slots currently allocated; 0 until the first write, then grows toward
117+ // max_slots on demand.
120118 int slots () const {
121- return static_cast <int >(buf_. shape (2 ));
119+ return buf_ ? static_cast <int >(buf_-> shape (2 )) : 0 ;
122120 }
123121
124122 private:
125- // Make room for `needed` slots, growing only if the pool is short: double
126- // until it fits, never past max_slots_. Cells keep their index, so growth is
127- // a zero-pad on the cell axis.
123+ // Make room for `needed` slots, allocating on first use and thereafter
124+ // growing only if the pool is short: double until it fits, never past
125+ // max_slots_. Cells keep their index, so growth is a zero-pad on the cell
126+ // axis. Every array here is placed on `s`, the stream the step runs on.
128127 void maybe_grow (int needed, StreamOrDevice s) {
128+ if (!buf_) {
129+ buf_ = ::mlx::core::zeros (
130+ ::mlx::core::Shape{1 , H_ , initial_slots_, D_ }, dtype_, s);
131+ }
129132 const int cur = slots ();
130133 if (needed <= cur) {
131134 return ;
@@ -137,16 +140,18 @@ class Pool {
137140 // The last doubling can overshoot; write() already bounds `needed` by
138141 // max_slots_, so clamping here cannot undershoot it.
139142 next = std::min (next, max_slots_);
140- const int H = static_cast <int >(buf_.shape (1 ));
141- const int D = static_cast <int >(buf_.shape (3 ));
142- Tensor pad =
143- ::mlx::core::zeros (::mlx::core::Shape{1 , H, next - cur, D}, dtype_);
144- buf_ = ::mlx::core::concatenate (std::vector<Tensor>{buf_, pad}, 2 , s);
143+ Tensor pad = ::mlx::core::zeros (
144+ ::mlx::core::Shape{1 , H_ , next - cur, D_ }, dtype_, s);
145+ buf_ = ::mlx::core::concatenate (std::vector<Tensor>{*buf_, pad}, 2 , s);
145146 }
146147
147148 ::mlx::core::Dtype dtype_;
148149 int max_slots_;
149- Tensor buf_;
150+ int initial_slots_;
151+ int H_ ;
152+ int D_ ;
153+ // Absent until the first write hands the pool a stream to allocate on.
154+ std::optional<Tensor> buf_;
150155};
151156
152157} // namespace mlx
0 commit comments