Unverified Commit 1548b42b authored by James Lamb's avatar James Lamb Committed by GitHub
Browse files

[R-package] [c++] add tighter multithreading control, avoid global OpenMP side...

[R-package] [c++] add tighter multithreading control, avoid global OpenMP side effects (fixes #4705, fixes #5102) (#6226)
parent e7979852
...@@ -30,8 +30,7 @@ get_omp_pragmas_without_num_threads() { ...@@ -30,8 +30,7 @@ get_omp_pragmas_without_num_threads() {
--include='*.h' \ --include='*.h' \
--include='*.hpp' \ --include='*.hpp' \
'pragma omp parallel' \ 'pragma omp parallel' \
| grep -v ' num_threads' \ | grep -v ' num_threads'
| grep -v 'openmp_wrapper.h'
} }
PROBLEMATIC_LINES=$( PROBLEMATIC_LINES=$(
get_omp_pragmas_without_num_threads get_omp_pragmas_without_num_threads
......
...@@ -432,6 +432,7 @@ file( ...@@ -432,6 +432,7 @@ file(
src/objective/*.cpp src/objective/*.cpp
src/network/*.cpp src/network/*.cpp
src/treelearner/*.cpp src/treelearner/*.cpp
src/utils/*.cpp
if(USE_CUDA) if(USE_CUDA)
src/treelearner/*.cu src/treelearner/*.cu
src/boosting/cuda/*.cpp src/boosting/cuda/*.cpp
......
...@@ -9,6 +9,7 @@ S3method(print,lgb.Booster) ...@@ -9,6 +9,7 @@ S3method(print,lgb.Booster)
S3method(set_field,lgb.Dataset) S3method(set_field,lgb.Dataset)
S3method(slice,lgb.Dataset) S3method(slice,lgb.Dataset)
S3method(summary,lgb.Booster) S3method(summary,lgb.Booster)
export(getLGBMthreads)
export(get_field) export(get_field)
export(lgb.Dataset) export(lgb.Dataset)
export(lgb.Dataset.construct) export(lgb.Dataset.construct)
...@@ -35,6 +36,7 @@ export(lgb.train) ...@@ -35,6 +36,7 @@ export(lgb.train)
export(lightgbm) export(lightgbm)
export(readRDS.lgb.Booster) export(readRDS.lgb.Booster)
export(saveRDS.lgb.Booster) export(saveRDS.lgb.Booster)
export(setLGBMthreads)
export(set_field) export(set_field)
export(slice) export(slice)
import(methods) import(methods)
......
...@@ -917,6 +917,8 @@ NULL ...@@ -917,6 +917,8 @@ NULL
#' the factor levels not being present in the output. #' the factor levels not being present in the output.
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
...@@ -1082,6 +1084,8 @@ predict.lgb.Booster <- function(object, ...@@ -1082,6 +1084,8 @@ predict.lgb.Booster <- function(object,
#' \link{predict.lgb.Booster}. #' \link{predict.lgb.Booster}.
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' library(lightgbm) #' library(lightgbm)
#' data(mtcars) #' data(mtcars)
#' X <- as.matrix(mtcars[, -1L]) #' X <- as.matrix(mtcars[, -1L])
...@@ -1224,6 +1228,8 @@ summary.lgb.Booster <- function(object, ...) { ...@@ -1224,6 +1228,8 @@ summary.lgb.Booster <- function(object, ...) {
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
...@@ -1289,6 +1295,8 @@ lgb.load <- function(filename = NULL, model_str = NULL) { ...@@ -1289,6 +1295,8 @@ lgb.load <- function(filename = NULL, model_str = NULL) {
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' library(lightgbm) #' library(lightgbm)
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
...@@ -1346,6 +1354,8 @@ lgb.save <- function(booster, filename, num_iteration = NULL) { ...@@ -1346,6 +1354,8 @@ lgb.save <- function(booster, filename, num_iteration = NULL) {
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' library(lightgbm) #' library(lightgbm)
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
...@@ -1396,6 +1406,8 @@ lgb.dump <- function(booster, num_iteration = NULL) { ...@@ -1396,6 +1406,8 @@ lgb.dump <- function(booster, num_iteration = NULL) {
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' # train a regression model #' # train a regression model
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
......
...@@ -780,6 +780,8 @@ Dataset <- R6::R6Class( ...@@ -780,6 +780,8 @@ Dataset <- R6::R6Class(
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
...@@ -837,6 +839,8 @@ lgb.Dataset <- function(data, ...@@ -837,6 +839,8 @@ lgb.Dataset <- function(data,
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
...@@ -913,6 +917,8 @@ lgb.Dataset.create.valid <- function(dataset, ...@@ -913,6 +917,8 @@ lgb.Dataset.create.valid <- function(dataset,
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
...@@ -942,6 +948,8 @@ lgb.Dataset.construct <- function(dataset) { ...@@ -942,6 +948,8 @@ lgb.Dataset.construct <- function(dataset) {
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
...@@ -975,6 +983,8 @@ dim.lgb.Dataset <- function(x) { ...@@ -975,6 +983,8 @@ dim.lgb.Dataset <- function(x) {
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
...@@ -1045,6 +1055,8 @@ dimnames.lgb.Dataset <- function(x) { ...@@ -1045,6 +1055,8 @@ dimnames.lgb.Dataset <- function(x) {
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
...@@ -1089,6 +1101,8 @@ slice.lgb.Dataset <- function(dataset, idxset) { ...@@ -1089,6 +1101,8 @@ slice.lgb.Dataset <- function(dataset, idxset) {
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
...@@ -1138,6 +1152,8 @@ get_field.lgb.Dataset <- function(dataset, field_name) { ...@@ -1138,6 +1152,8 @@ get_field.lgb.Dataset <- function(dataset, field_name) {
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
...@@ -1177,6 +1193,8 @@ set_field.lgb.Dataset <- function(dataset, field_name, data) { ...@@ -1177,6 +1193,8 @@ set_field.lgb.Dataset <- function(dataset, field_name, data) {
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
...@@ -1207,6 +1225,8 @@ lgb.Dataset.set.categorical <- function(dataset, categorical_feature) { ...@@ -1207,6 +1225,8 @@ lgb.Dataset.set.categorical <- function(dataset, categorical_feature) {
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' # create training Dataset #' # create training Dataset
#' data(agaricus.train, package ="lightgbm") #' data(agaricus.train, package ="lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
...@@ -1240,6 +1260,8 @@ lgb.Dataset.set.reference <- function(dataset, reference) { ...@@ -1240,6 +1260,8 @@ lgb.Dataset.set.reference <- function(dataset, reference) {
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
......
...@@ -51,6 +51,8 @@ CVBooster <- R6::R6Class( ...@@ -51,6 +51,8 @@ CVBooster <- R6::R6Class(
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
......
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
......
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' Logit <- function(x) log(x / (1.0 - x)) #' Logit <- function(x) log(x / (1.0 - x))
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
......
...@@ -29,6 +29,8 @@ ...@@ -29,6 +29,8 @@
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
......
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
......
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' Logit <- function(x) { #' Logit <- function(x) {
#' log(x / (1.0 - x)) #' log(x / (1.0 - x))
#' } #' }
......
...@@ -16,7 +16,10 @@ ...@@ -16,7 +16,10 @@
#' @return \code{lgb.Booster} (the same `model` object that was passed as input, invisibly). #' @return \code{lgb.Booster} (the same `model` object that was passed as input, invisibly).
#' @seealso \link{lgb.make_serializable}, \link{lgb.drop_serialized}. #' @seealso \link{lgb.make_serializable}, \link{lgb.drop_serialized}.
#' @examples #' @examples
#' \donttest{
#' library(lightgbm) #' library(lightgbm)
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data("agaricus.train") #' data("agaricus.train")
#' model <- lightgbm( #' model <- lightgbm(
#' agaricus.train$data #' agaricus.train$data
...@@ -33,6 +36,7 @@ ...@@ -33,6 +36,7 @@
#' model_new$check_null_handle() #' model_new$check_null_handle()
#' lgb.restore_handle(model_new) #' lgb.restore_handle(model_new)
#' model_new$check_null_handle() #' model_new$check_null_handle()
#' }
#' @export #' @export
lgb.restore_handle <- function(model) { lgb.restore_handle <- function(model) {
if (!.is_Booster(x = model)) { if (!.is_Booster(x = model)) {
......
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
#' #'
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
......
#' @name setLGBMThreads
#' @title Set maximum number of threads used by LightGBM
#' @description LightGBM attempts to speed up many operations by using multi-threading.
#' The number of threads used in those operations can be controlled via the
#' \code{num_threads} parameter passed through \code{params} to functions like
#' \link{lgb.train} and \link{lgb.Dataset}. However, some operations (like materializing
#' a model from a text file) are done via code paths that don't explicitly accept thread-control
#' configuration.
#'
#' Use this function to set the maximum number of threads LightGBM will use for such operations.
#'
#' This function affects all LightGBM operations in the same process.
#'
#' So, for example, if you call \code{setLGBMthreads(4)}, no other multi-threaded LightGBM
#' operation in the same process will use more than 4 threads.
#'
#' Call \code{setLGBMthreads(-1)} to remove this limitation.
#' @param num_threads maximum number of threads to be used by LightGBM in multi-threaded operations
#' @return NULL
#' @seealso \link{getLGBMthreads}
#' @export
setLGBMthreads <- function(num_threads) {
.Call(
LGBM_SetMaxThreads_R,
num_threads
)
return(invisible(NULL))
}
#' @name getLGBMThreads
#' @title Get default number of threads used by LightGBM
#' @description LightGBM attempts to speed up many operations by using multi-threading.
#' The number of threads used in those operations can be controlled via the
#' \code{num_threads} parameter passed through \code{params} to functions like
#' \link{lgb.train} and \link{lgb.Dataset}. However, some operations (like materializing
#' a model from a text file) are done via code paths that don't explicitly accept thread-control
#' configuration.
#'
#' Use this function to see the default number of threads LightGBM will use for such operations.
#' @return number of threads as an integer. \code{-1} means that in situations where parameter \code{num_threads} is
#' not explicitly supplied, LightGBM will choose a number of threads to use automatically.
#' @seealso \link{setLGBMthreads}
#' @export
getLGBMthreads <- function() {
out <- 0L
.Call(
LGBM_GetMaxThreads_R,
out
)
return(out)
}
...@@ -12,6 +12,8 @@ ...@@ -12,6 +12,8 @@
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' library(lightgbm) #' library(lightgbm)
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
......
...@@ -22,6 +22,8 @@ ...@@ -22,6 +22,8 @@
#' @examples #' @examples
#' \donttest{ #' \donttest{
#' library(lightgbm) #' library(lightgbm)
#' \dontshow{setLGBMthreads(2L)}
#' \dontshow{data.table::setDTthreads(1L)}
#' data(agaricus.train, package = "lightgbm") #' data(agaricus.train, package = "lightgbm")
#' train <- agaricus.train #' train <- agaricus.train
#' dtrain <- lgb.Dataset(train$data, label = train$label) #' dtrain <- lgb.Dataset(train$data, label = train$label)
......
...@@ -21,6 +21,8 @@ be directly used with an \code{lgb.Dataset} object. ...@@ -21,6 +21,8 @@ be directly used with an \code{lgb.Dataset} object.
} }
\examples{ \examples{
\donttest{ \donttest{
\dontshow{setLGBMthreads(2L)}
\dontshow{data.table::setDTthreads(1L)}
data(agaricus.train, package = "lightgbm") data(agaricus.train, package = "lightgbm")
train <- agaricus.train train <- agaricus.train
dtrain <- lgb.Dataset(train$data, label = train$label) dtrain <- lgb.Dataset(train$data, label = train$label)
......
...@@ -28,6 +28,8 @@ Since row names are irrelevant, it is recommended to use \code{colnames} directl ...@@ -28,6 +28,8 @@ Since row names are irrelevant, it is recommended to use \code{colnames} directl
} }
\examples{ \examples{
\donttest{ \donttest{
\dontshow{setLGBMthreads(2L)}
\dontshow{data.table::setDTthreads(1L)}
data(agaricus.train, package = "lightgbm") data(agaricus.train, package = "lightgbm")
train <- agaricus.train train <- agaricus.train
dtrain <- lgb.Dataset(train$data, label = train$label) dtrain <- lgb.Dataset(train$data, label = train$label)
......
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/multithreading.R
\name{getLGBMThreads}
\alias{getLGBMThreads}
\alias{getLGBMthreads}
\title{Get default number of threads used by LightGBM}
\usage{
getLGBMthreads()
}
\value{
number of threads as an integer. \code{-1} means that in situations where parameter \code{num_threads} is
not explicitly supplied, LightGBM will choose a number of threads to use automatically.
}
\description{
LightGBM attempts to speed up many operations by using multi-threading.
The number of threads used in those operations can be controlled via the
\code{num_threads} parameter passed through \code{params} to functions like
\link{lgb.train} and \link{lgb.Dataset}. However, some operations (like materializing
a model from a text file) are done via code paths that don't explicitly accept thread-control
configuration.
Use this function to see the default number of threads LightGBM will use for such operations.
}
\seealso{
\link{setLGBMthreads}
}
...@@ -32,6 +32,8 @@ Get one attribute of a \code{lgb.Dataset} ...@@ -32,6 +32,8 @@ Get one attribute of a \code{lgb.Dataset}
} }
\examples{ \examples{
\donttest{ \donttest{
\dontshow{setLGBMthreads(2L)}
\dontshow{data.table::setDTthreads(1L)}
data(agaricus.train, package = "lightgbm") data(agaricus.train, package = "lightgbm")
train <- agaricus.train train <- agaricus.train
dtrain <- lgb.Dataset(train$data, label = train$label) dtrain <- lgb.Dataset(train$data, label = train$label)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment