warpgbm 0.1.22__tar.gz → 0.1.23__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {warpgbm-0.1.22/warpgbm.egg-info → warpgbm-0.1.23}/PKG-INFO +1 -1
  2. {warpgbm-0.1.22 → warpgbm-0.1.23}/pyproject.toml +1 -1
  3. {warpgbm-0.1.22 → warpgbm-0.1.23}/tests/test_fit_predict_corr.py +9 -1
  4. warpgbm-0.1.23/version.txt +1 -0
  5. {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/core.py +147 -11
  6. {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/cuda/best_split_kernel.cu +1 -1
  7. {warpgbm-0.1.22 → warpgbm-0.1.23/warpgbm.egg-info}/PKG-INFO +1 -1
  8. warpgbm-0.1.22/version.txt +0 -1
  9. {warpgbm-0.1.22 → warpgbm-0.1.23}/LICENSE +0 -0
  10. {warpgbm-0.1.22 → warpgbm-0.1.23}/MANIFEST.in +0 -0
  11. {warpgbm-0.1.22 → warpgbm-0.1.23}/README.md +0 -0
  12. {warpgbm-0.1.22 → warpgbm-0.1.23}/setup.cfg +0 -0
  13. {warpgbm-0.1.22 → warpgbm-0.1.23}/setup.py +0 -0
  14. {warpgbm-0.1.22 → warpgbm-0.1.23}/tests/__init__.py +0 -0
  15. {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/__init__.py +0 -0
  16. {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/cuda/__init__.py +0 -0
  17. {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/cuda/binner.cu +0 -0
  18. {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/cuda/histogram_kernel.cu +0 -0
  19. {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/cuda/node_kernel.cpp +0 -0
  20. {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/cuda/predict.cu +0 -0
  21. {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm.egg-info/SOURCES.txt +0 -0
  22. {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm.egg-info/dependency_links.txt +0 -0
  23. {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm.egg-info/requires.txt +0 -0
  24. {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: warpgbm
3
- Version: 0.1.22
3
+ Version: 0.1.23
4
4
  Summary: A fast GPU-accelerated Gradient Boosted Decision Tree library with PyTorch + CUDA
5
5
  License: GNU GENERAL PUBLIC LICENSE
6
6
  Version 3, 29 June 2007
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "warpgbm"
7
- version = "0.1.22"
7
+ version = "0.1.23"
8
8
  description = "A fast GPU-accelerated Gradient Boosted Decision Tree library with PyTorch + CUDA"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -29,7 +29,15 @@ def test_fit_predictpytee_correlation():
29
29
  )
30
30
 
31
31
  start_fit = time.time()
32
- model.fit(X, y, era_id=era)
32
+ model.fit(
33
+ X,
34
+ y,
35
+ era_id=era,
36
+ X_eval=X,
37
+ y_eval=y,
38
+ eval_every_n_trees=10,
39
+ early_stopping_rounds=1,
40
+ )
33
41
  fit_time = time.time() - start_fit
34
42
  print(f" Fit time: {fit_time:.3f} seconds")
35
43
 
@@ -0,0 +1 @@
1
+ 0.1.23
@@ -70,6 +70,7 @@ class WarpGBM(BaseEstimator, RegressorMixin):
70
70
  self.rows_per_thread = rows_per_thread
71
71
  self.L2_reg = L2_reg
72
72
  self.L1_reg = L1_reg
73
+ self.forest = [{} for _ in range(self.n_estimators)]
73
74
 
74
75
  def _validate_hyperparams(self, **kwargs):
75
76
  # Type checks
@@ -122,9 +123,95 @@ class WarpGBM(BaseEstimator, RegressorMixin):
122
123
  f"Invalid histogram_computer: {kwargs['histogram_computer']}. Choose from {list(histogram_kernels.keys())}."
123
124
  )
124
125
 
125
- def fit(self, X, y, era_id=None):
126
+ def validate_fit_params(
127
+ self, X, y, era_id, X_eval, y_eval, eval_every_n_trees, early_stopping_rounds
128
+ ):
129
+ # ─── Required: X and y ───
130
+ if not isinstance(X, np.ndarray) or not isinstance(y, np.ndarray):
131
+ raise TypeError("X and y must be numpy arrays.")
132
+ if X.ndim != 2:
133
+ raise ValueError(f"X must be 2-dimensional, got shape {X.shape}")
134
+ if y.ndim != 1:
135
+ raise ValueError(f"y must be 1-dimensional, got shape {y.shape}")
136
+ if X.shape[0] != y.shape[0]:
137
+ raise ValueError(
138
+ f"X and y must have the same number of rows. Got {X.shape[0]} and {y.shape[0]}."
139
+ )
140
+
141
+ # ─── Optional: era_id ───
142
+ if era_id is not None:
143
+ if not isinstance(era_id, np.ndarray):
144
+ raise TypeError("era_id must be a numpy array.")
145
+ if era_id.ndim != 1:
146
+ raise ValueError(
147
+ f"era_id must be 1-dimensional, got shape {era_id.shape}"
148
+ )
149
+ if len(era_id) != len(y):
150
+ raise ValueError(
151
+ f"era_id must have same length as y. Got {len(era_id)} and {len(y)}."
152
+ )
153
+
154
+ # ─── Optional: Eval Set ───
155
+ eval_args = [X_eval, y_eval, eval_every_n_trees]
156
+ if any(arg is not None for arg in eval_args):
157
+ # Require all of them
158
+ if X_eval is None or y_eval is None or eval_every_n_trees is None:
159
+ raise ValueError(
160
+ "If using eval set, X_eval, y_eval, and eval_every_n_trees must all be defined."
161
+ )
162
+
163
+ if not isinstance(X_eval, np.ndarray) or not isinstance(y_eval, np.ndarray):
164
+ raise TypeError("X_eval and y_eval must be numpy arrays.")
165
+ if X_eval.ndim != 2:
166
+ raise ValueError(
167
+ f"X_eval must be 2-dimensional, got shape {X_eval.shape}"
168
+ )
169
+ if y_eval.ndim != 1:
170
+ raise ValueError(
171
+ f"y_eval must be 1-dimensional, got shape {y_eval.shape}"
172
+ )
173
+ if X_eval.shape[0] != y_eval.shape[0]:
174
+ raise ValueError(
175
+ f"X_eval and y_eval must have same number of rows. Got {X_eval.shape[0]} and {y_eval.shape[0]}."
176
+ )
177
+
178
+ if not isinstance(eval_every_n_trees, int) or eval_every_n_trees <= 0:
179
+ raise ValueError(
180
+ f"eval_every_n_trees must be a positive integer, got {eval_every_n_trees}."
181
+ )
182
+
183
+ if early_stopping_rounds is not None:
184
+ if (
185
+ not isinstance(early_stopping_rounds, int)
186
+ or early_stopping_rounds <= 0
187
+ ):
188
+ raise ValueError(
189
+ f"early_stopping_rounds must be a positive integer, got {early_stopping_rounds}."
190
+ )
191
+ else:
192
+ # No early stopping = set to "never trigger"
193
+ early_stopping_rounds = self.n_estimators + 1
194
+
195
+ return early_stopping_rounds # May have been defaulted here
196
+
197
+ def fit(
198
+ self,
199
+ X,
200
+ y,
201
+ era_id=None,
202
+ X_eval=None,
203
+ y_eval=None,
204
+ eval_every_n_trees=None,
205
+ early_stopping_rounds=None,
206
+ ):
207
+ early_stopping_rounds = self.validate_fit_params(
208
+ X, y, era_id, X_eval, y_eval, eval_every_n_trees, early_stopping_rounds
209
+ )
210
+
126
211
  if era_id is None:
127
212
  era_id = np.ones(X.shape[0], dtype="int32")
213
+
214
+ # Train data preprocessing
128
215
  self.bin_indices, era_indices, self.bin_edges, self.unique_eras, self.Y_gpu = (
129
216
  self.preprocess_gpu_data(X, y, era_id)
130
217
  )
@@ -137,8 +224,23 @@ class WarpGBM(BaseEstimator, RegressorMixin):
137
224
  self.best_bins = torch.zeros(
138
225
  self.num_features, device=self.device, dtype=torch.int32
139
226
  )
227
+
228
+ # ─── Optional Eval Set ───
229
+ if X_eval is not None and y_eval is not None:
230
+ self.bin_indices_eval = self.bin_data_with_existing_edges(X_eval)
231
+ self.Y_gpu_eval = torch.from_numpy(y_eval).to(torch.float32).to(self.device)
232
+ self.eval_every_n_trees = eval_every_n_trees
233
+ self.early_stopping_rounds = early_stopping_rounds
234
+ else:
235
+ self.bin_indices_eval = None
236
+ self.Y_gpu_eval = None
237
+ self.eval_every_n_trees = None
238
+ self.early_stopping_rounds = None
239
+
240
+ # ─── Grow the forest ───
140
241
  with torch.no_grad():
141
- self.forest = self.grow_forest()
242
+ self.grow_forest()
243
+
142
244
  return self
143
245
 
144
246
  def preprocess_gpu_data(self, X_np, Y_np, era_id_np):
@@ -292,11 +394,34 @@ class WarpGBM(BaseEstimator, RegressorMixin):
292
394
  "right": right_child,
293
395
  }
294
396
 
397
+ def compute_eval(self, i):
398
+ if self.eval_every_n_trees == None:
399
+ return
400
+
401
+ if i % self.eval_every_n_trees == 0:
402
+ eval_preds = self.predict_binned(self.bin_indices_eval)
403
+ eval_loss = ((self.Y_gpu_eval - eval_preds) ** 2).mean().item()
404
+ self.eval_loss.append(eval_loss)
405
+
406
+ train_loss = ((self.Y_gpu - self.gradients) ** 2).mean().item()
407
+ self.training_loss.append(train_loss)
408
+
409
+ if len(self.eval_loss) > self.early_stopping_rounds:
410
+ if self.eval_loss[-self.early_stopping_rounds] < self.eval_loss[-1]:
411
+ self.stop = True
412
+
413
+ print(
414
+ f"🌲 Tree {i+1}/{self.n_estimators} | Train MSE: {train_loss:.6f} | Eval MSE: {eval_loss:.6f}"
415
+ )
416
+
417
+ del eval_preds, eval_loss, train_loss
418
+
295
419
  def grow_forest(self):
296
- forest = [{} for _ in range(self.n_estimators)]
297
420
  self.training_loss = []
421
+ self.eval_loss = [] # <-- if eval set is given
422
+ self.stop = False
298
423
 
299
- for i in tqdm(range(self.n_estimators)):
424
+ for i in range(self.n_estimators):
300
425
  self.residual = self.Y_gpu - self.gradients
301
426
 
302
427
  self.root_gradient_histogram, self.root_hessian_histogram = (
@@ -309,21 +434,21 @@ class WarpGBM(BaseEstimator, RegressorMixin):
309
434
  self.root_node_indices,
310
435
  depth=0,
311
436
  )
312
- forest[i] = tree
313
- # loss = ((self.Y_gpu - self.gradients) ** 2).mean().item()
314
- # self.training_loss.append(loss)
315
- # print(f"🌲 Tree {i+1}/{self.n_estimators} - MSE: {loss:.6f}")
437
+ self.forest[i] = tree
438
+
439
+ self.compute_eval(i)
440
+
441
+ if self.stop:
442
+ break
316
443
 
317
444
  print("Finished training forest.")
318
- return forest
319
445
 
320
- def predict(self, X_np):
446
+ def bin_data_with_existing_edges(self, X_np):
321
447
  X_tensor = torch.from_numpy(X_np).to(torch.float32).pin_memory()
322
448
  num_samples = X_tensor.size(0)
323
449
  bin_indices = torch.zeros(
324
450
  (num_samples, self.num_features), dtype=torch.int8, device=self.device
325
451
  )
326
-
327
452
  with torch.no_grad():
328
453
  for f in range(self.num_features):
329
454
  X_f = X_tensor[:, f].to(self.device, non_blocking=True)
@@ -332,10 +457,16 @@ class WarpGBM(BaseEstimator, RegressorMixin):
332
457
  node_kernel.custom_cuda_binner(X_f, bin_edges_f, bin_indices_f)
333
458
  bin_indices[:, f] = bin_indices_f
334
459
 
460
+ return bin_indices
461
+
462
+ def predict_binned(self, bin_indices):
463
+ num_samples = bin_indices.size(0)
464
+
335
465
  tree_tensor = torch.stack(
336
466
  [
337
467
  self.flatten_tree(tree, max_nodes=2 ** (self.max_depth + 1))
338
468
  for tree in self.forest
469
+ if tree
339
470
  ]
340
471
  ).to(self.device)
341
472
 
@@ -344,6 +475,11 @@ class WarpGBM(BaseEstimator, RegressorMixin):
344
475
  bin_indices.contiguous(), tree_tensor.contiguous(), self.learning_rate, out
345
476
  )
346
477
 
478
+ return out
479
+
480
+ def predict(self, X_np):
481
+ bin_indices = self.bin_data_with_existing_edges(X_np)
482
+ out = self.predict_binned(bin_indices)
347
483
  return out.cpu().numpy()
348
484
 
349
485
  def flatten_tree(self, tree, max_nodes):
@@ -38,7 +38,7 @@ __global__ void best_split_kernel_global_only(
38
38
 
39
39
  if (H_L >= min_child_samples && H_R >= min_child_samples)
40
40
  {
41
- float gain = (G_L * G_L) / (H_L + eps) + (G_R * G_R) / (H_R + eps);
41
+ float gain = (G_L * G_L) / (H_L + eps) + (G_R * G_R) / (H_R + eps) - (G_total * G_total) / (H_total + eps);
42
42
  if (gain > best_gain)
43
43
  {
44
44
  best_gain = gain;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: warpgbm
3
- Version: 0.1.22
3
+ Version: 0.1.23
4
4
  Summary: A fast GPU-accelerated Gradient Boosted Decision Tree library with PyTorch + CUDA
5
5
  License: GNU GENERAL PUBLIC LICENSE
6
6
  Version 3, 29 June 2007
@@ -1 +0,0 @@
1
- 0.1.22
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes