671
pulse_ml_models_sent();
672
}
673
674
-static void ml_dimension_update_models(ml_worker_t *worker, ml_dimension_t *dim)
674
+bool ml_dimension_train_model_precheck(enum ml_metric_type mt,
675
+ bool has_received_downstream_model,
676
+ bool training_in_progress,
677
+ enum ml_worker_result *worker_res)
678
+{
679
+ if (has_received_downstream_model) {
680
+ *worker_res = ML_WORKER_RESULT_DOWNSTREAM_MODEL_SUPPLIED;
681
+ return true;
682
+ }
683
+
684
+ if (mt == METRIC_TYPE_CONSTANT) {
685
+ *worker_res = ML_WORKER_RESULT_OK;
686
+ return true;
687
+ }
688
+
689
+ if (training_in_progress) {
690
+ *worker_res = ML_WORKER_RESULT_TRAINING_IN_PROGRESS;
691
+ return true;
692
+ }
693
+
694
+ return false;
695
+}
696
+
697
+bool ml_should_requeue_create_new_model(enum ml_worker_result worker_res)
698
+{
699
+ // TRAINING_IN_PROGRESS keeps requeueing so the dim stays in the periodic
700
+ // retrain cycle; the worker loop is paced by Cfg.train_every, so this is
701
+ // not a tight CPU spin.
702
+ return worker_res != ML_WORKER_RESULT_NULL_ACQUIRED_DIMENSION &&
703
+ worker_res != ML_WORKER_RESULT_DOWNSTREAM_MODEL_SUPPLIED;
704
+}
705
+
706
+bool ml_should_publish_model_update(bool host_running,
707
+ uint32_t current_generation,
708
+ uint32_t expected_generation,
709
+ bool *training_in_progress)
710
+{
711
+ if (!host_running || current_generation != expected_generation) {
712
+ if (training_in_progress)
713
+ *training_in_progress = false;
714
+ return false;
715
+ }
716
+
717
+ return true;
718
+}
719
+
720
+static bool ml_dimension_update_models(ml_worker_t *worker, ml_dimension_t *dim, uint32_t expected_generation, bool from_downstream)
721
{
722
worker_is_busy(WORKER_TRAIN_UPDATE_MODELS);
723
724
spinlock_lock(&dim->slock);
725
726
ml_host_t *host = (ml_host_t *) dim->rd->rrdset->rrdhost->ml_host;
681
- if (!host || !host->ml_running) {
682
- dim->training_in_progress = false;
727
+ if (!ml_should_publish_model_update(host && host->ml_running,
728
+ dim->reset_generation,
729
+ expected_generation,
730
+ &dim->training_in_progress)) {
731
spinlock_unlock(&dim->slock);
684
- return;
732
+ return false;
733
}
734
735
+ // Mark the dim as downstream-supplied only after the publish-check passes
736
+ // and under the same slock as the install. Setting it earlier would risk
737
+ // suppressing local training if the install was cancelled.
738
+ if (from_downstream)
739
+ dim->has_received_downstream_model = true;
740
+
741
if (dim->km_contexts.size() < Cfg.num_models_to_use) {
742
dim->km_contexts.emplace_back(dim->kmeans);
743
} else {
766
dim->suppression_anomaly_counter = 0;
767
dim->suppression_window_counter = 0;
768
715
- // Add the newly generated model to the list of pending models to flush
769
+ // Add the latest model to the list of pending models to flush.
770
ml_model_info_t model_info;
771
nd_uuid_t *rd_uuid = uuidmap_uuid_ptr(dim->rd->uuid);
772
uuid_copy(model_info.metric_uuid, *rd_uuid);
779
dim->training_in_progress = false;
780
781
spinlock_unlock(&dim->slock);
782
+ return true;
783
}
784
785
static enum ml_worker_result
788
worker_is_busy(WORKER_TRAIN_QUERY);
789
790
spinlock_lock(&dim->slock);
736
- if (dim->mt == METRIC_TYPE_CONSTANT) {
791
+ ml_worker_result precheck;
792
+ if (ml_dimension_train_model_precheck(dim->mt,
793
+ dim->has_received_downstream_model,
794
+ dim->training_in_progress,
795
+ &precheck)) {
796
+ if (precheck == ML_WORKER_RESULT_DOWNSTREAM_MODEL_SUPPLIED)
797
+ dim->create_new_model_queued = false;
798
spinlock_unlock(&dim->slock);
738
- return ML_WORKER_RESULT_OK;
799
+ return precheck;
800
}
801
741
- // Check if training is already in progress for this dimension
742
- // If so, skip this training request to prevent concurrent access to dim->kmeans
743
- if (dim->training_in_progress) {
744
- spinlock_unlock(&dim->slock);
745
- return ML_WORKER_RESULT_OK;
746
- }
747
-
748
- // Mark training as in progress
802
+ // Mark training as in progress and snapshot the generation so that
803
+ // ml_dimension_update_models() can detect a stop/reset that happened
804
+ // while training was running.
805
dim->training_in_progress = true;
806
+ uint32_t generation = dim->reset_generation;
807
spinlock_unlock(&dim->slock);
808
809
auto P = ml_dimension_calculated_numbers(worker, dim);
836
worker->scratch_training_cns, training_response.total_values,
837
worker->training_cns, training_response.total_values
838
};
782
-
839
+
840
// Calculate dynamic sampling ratio based on expected output size
841
// After diff and smooth, we'll have approximately this many vectors
842
size_t expected_vectors = training_response.total_values;
843
if (Cfg.diff_n > 0) expected_vectors--;
844
if (smoothing_window > 1) expected_vectors = expected_vectors - smoothing_window + 1;
845
expected_vectors = expected_vectors - Cfg.lag_n;
789
-
846
+
847
double sampling_ratio = 1.0;
848
if (expected_vectors > Cfg.max_training_vectors) {
849
sampling_ratio = (double)Cfg.max_training_vectors / expected_vectors;
857
}
858
859
// update models
803
- ml_dimension_update_models(worker, dim);
860
+ (void) ml_dimension_update_models(worker, dim, generation, /*from_downstream=*/false);
861
862
return worker_result;
863
}
1110
auto &um = host->context_anomaly_rate;
1111
auto it = um.find(key);
1112
if (it == um.end()) {
1056
- um[key] = ml_context_anomaly_rate_t {
1113
+ STRING *owned_key = string_dup(key);
1114
+ auto insert_result = um.emplace(owned_key, ml_context_anomaly_rate_t {
1115
.rd = NULL,
1116
.normal_dimensions = 0,
1117
.anomalous_dimensions = 0
1060
- };
1061
- it = um.find(key);
1118
+ });
1119
+ if (!insert_result.second)
1120
+ string_freez(owned_key);
1121
+ it = insert_result.first;
1122
}
1123
1124
it->second.anomalous_dimensions += chart_mls.num_anomalous_dimensions;
1144
ml_update_host_and_detection_rate_charts(host, host->host_anomaly_rate * 10000.0, owa);
1145
} else {
1146
host->host_anomaly_rate = 0.0;
1087
-
1088
- auto &um = host->context_anomaly_rate;
1089
- for (auto &entry: um) {
1090
- entry.second = ml_context_anomaly_rate_t {
1091
- .rd = NULL,
1092
- .normal_dimensions = 0,
1093
- .anomalous_dimensions = 0
1094
- };
1095
- }
1147
}
1148
}
1149
1270
}
1271
1272
static enum ml_worker_result ml_worker_add_existing_model(ml_worker_t *worker, ml_request_add_existing_model_t req) {
1222
- UNUSED(worker);
1223
- UNUSED(req);
1224
-
1273
AcquiredDimension AcqDim(req.DLI);
1274
1275
if (!AcqDim.acquired()) {
1282
return ML_WORKER_RESULT_OK;
1283
}
1284
1237
- // Check if training is in progress and skip if so to avoid race condition
1285
+ ml_host_t *host = (ml_host_t *) Dim->rd->rrdset->rrdhost->ml_host;
1286
+ if (!host || !host->ml_running) {
1287
+ pulse_ml_models_ignored();
1288
+ return ML_WORKER_RESULT_OK;
1289
+ }
1290
+
1291
spinlock_lock(&Dim->slock);
1292
+
1293
+ // Loop detection: skip if we already have this exact model.
1294
+ // The (after, before) pair uniquely identifies a model per dimension and is
1295
+ // preserved across hops, so a model that loops back is detected as a duplicate.
1296
+ for (const auto &km : Dim->km_contexts) {
1297
+ if (km.after == req.inlined_km.after && km.before == req.inlined_km.before) {
1298
+ spinlock_unlock(&Dim->slock);
1299
+ pulse_ml_models_ignored();
1300
+ return ML_WORKER_RESULT_OK;
1301
+ }
1302
+ }
1303
+
1304
+ // Reject models that are not newer than the newest accepted model. This
1305
+ // prevents an older model from being re-accepted after it has been evicted
1306
+ // from km_contexts and later loops back from downstream.
1307
+ if (!Dim->km_contexts.empty()) {
1308
+ const auto &latest_km = Dim->km_contexts.back();
1309
+ if (req.inlined_km.before <= latest_km.before) {
1310
+ spinlock_unlock(&Dim->slock);
1311
+ pulse_ml_models_ignored();
1312
+ return ML_WORKER_RESULT_OK;
1313
+ }
1314
+ }
1315
+
1316
+ // Skip if training is in progress to avoid race condition.
1317
if (Dim->training_in_progress) {
1318
spinlock_unlock(&Dim->slock);
1319
pulse_ml_models_ignored();
1320
return ML_WORKER_RESULT_OK;
1321
}
1244
- spinlock_unlock(&Dim->slock);
1322
1246
- // Safe without Dim->slock: per-host work is serialized through a single worker queue,
1247
- // and stop/reset no longer writes Dim->kmeans from non-worker threads.
1323
+ // Stage the incoming kmeans into the dim's working buffer; the actual
1324
+ // install into km_contexts and the has_received_downstream_model flag-set
1325
+ // happen inside ml_dimension_update_models() under the same slock as the
1326
+ // publish-check, so a concurrent ml_host_stop() either commits both or
1327
+ // cancels both.
1328
Dim->kmeans = req.inlined_km;
1249
- ml_dimension_update_models(worker, Dim);
1250
- pulse_ml_models_received();
1329
+ uint32_t generation = Dim->reset_generation;
1330
+ spinlock_unlock(&Dim->slock);
1331
+ if (ml_dimension_update_models(worker, Dim, generation, /*from_downstream=*/true))
1332
+ pulse_ml_models_received();
1333
+
1334
return ML_WORKER_RESULT_OK;
1335
}
1336
1379
switch (item.type) {
1380
case ML_QUEUE_ITEM_TYPE_CREATE_NEW_MODEL: {
1381
worker_res = ml_worker_create_new_model(worker, item.create_new_model);
1299
- if (worker_res != ML_WORKER_RESULT_NULL_ACQUIRED_DIMENSION) {
1382
+ if (ml_should_requeue_create_new_model(worker_res)) {
1383
ml_queue_push(worker->queue, item);
1384
}
1385
break;
1429
case ML_WORKER_RESULT_CHART_UNDER_REPLICATION:
1430
loop_stats.item_result_chart_under_replication = 1;
1431
break;
1432
+ case ML_WORKER_RESULT_DOWNSTREAM_MODEL_SUPPLIED:
1433
+ loop_stats.item_result_ok = 1;
1434
+ break;
1435
+ case ML_WORKER_RESULT_TRAINING_IN_PROGRESS:
1436
+ loop_stats.item_result_ok = 1;
1437
+ break;
1438
}
1439
1440
netdata_mutex_lock(&worker->nd_mutex);