-
Notifications
You must be signed in to change notification settings - Fork 25.2k
[ML] prevent accidentally asking for more resources when scaling down and improve scaling size estimations #74691
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6e35a2b
ed2a12b
ba0b815
a866c11
8cda42f
26f65f5
29daa11
4af1e67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,7 @@ | |
|
||
public final class NativeMemoryCalculator { | ||
|
||
private static final long STATIC_JVM_UPPER_THRESHOLD = ByteSizeValue.ofGb(2).getBytes(); | ||
static final long MINIMUM_AUTOMATIC_NODE_SIZE = ByteSizeValue.ofGb(1).getBytes(); | ||
private static final long OS_OVERHEAD = ByteSizeValue.ofMb(200L).getBytes(); | ||
|
||
|
@@ -80,15 +81,11 @@ public static long calculateApproxNecessaryNodeSize(long nativeMachineMemory, Lo | |
if (useAuto) { | ||
// TODO utilize official ergonomic JVM size calculations when available. | ||
jvmSize = jvmSize == null ? dynamicallyCalculateJvmSizeFromNativeMemorySize(nativeMachineMemory) : jvmSize; | ||
// We use a Math.floor here to ensure we have AT LEAST enough memory given rounding. | ||
int modelMemoryPercent = (int)Math.floor(modelMemoryPercent( | ||
nativeMachineMemory + jvmSize + OS_OVERHEAD, | ||
jvmSize, | ||
maxMemoryPercent, | ||
true)); | ||
// We calculate the inverse percentage of `nativeMachineMemory + OS_OVERHEAD` as `OS_OVERHEAD` is always present | ||
// on the native memory side and we need to account for it when we invert the model memory percentage | ||
return Math.max((long)Math.ceil((100.0/modelMemoryPercent) * (nativeMachineMemory + OS_OVERHEAD)), MINIMUM_AUTOMATIC_NODE_SIZE); | ||
// We haven't reached our 90% threshold, so, simply summing up the values is adequate | ||
if ((jvmSize + OS_OVERHEAD)/(double)nativeMachineMemory > 0.1) { | ||
return Math.max(nativeMachineMemory + jvmSize + OS_OVERHEAD, MINIMUM_AUTOMATIC_NODE_SIZE); | ||
} | ||
return Math.round((nativeMachineMemory/0.9)); | ||
} | ||
return (long) ((100.0/maxMemoryPercent) * nativeMachineMemory); | ||
} | ||
|
@@ -118,18 +115,11 @@ public static double modelMemoryPercent(long machineMemory, Long jvmSize, int ma | |
return maxMemoryPercent; | ||
} | ||
|
||
public static int modelMemoryPercent(long machineMemory, int maxMemoryPercent, boolean useAuto) { | ||
return (int)Math.ceil(modelMemoryPercent(machineMemory, | ||
null, | ||
maxMemoryPercent, | ||
useAuto)); | ||
} | ||
|
||
private static long allowedBytesForMl(long machineMemory, Long jvmSize, int maxMemoryPercent, boolean useAuto) { | ||
static long allowedBytesForMl(long machineMemory, Long jvmSize, int maxMemoryPercent, boolean useAuto) { | ||
if (useAuto && jvmSize != null) { | ||
// It is conceivable that there is a machine smaller than 200MB. | ||
// If the administrator wants to use the auto configuration, the node should be larger. | ||
if (machineMemory - jvmSize < OS_OVERHEAD || machineMemory == 0) { | ||
if (machineMemory - jvmSize <= OS_OVERHEAD || machineMemory == 0) { | ||
return machineMemory / 100; | ||
} | ||
// This calculation is dynamic and designed to maximally take advantage of the underlying machine for machine learning | ||
|
@@ -139,8 +129,8 @@ private static long allowedBytesForMl(long machineMemory, Long jvmSize, int maxM | |
// 2GB node -> 66% | ||
// 16GB node -> 87% | ||
// 64GB node -> 90% | ||
long memoryPercent = Math.min(90, (int)Math.ceil(((machineMemory - jvmSize - OS_OVERHEAD) / (double)machineMemory) * 100.0D)); | ||
return (long)(machineMemory * (memoryPercent / 100.0)); | ||
double memoryProportion = Math.min(0.90, (machineMemory - jvmSize - OS_OVERHEAD) / (double)machineMemory); | ||
return Math.round(machineMemory * memoryProportion); | ||
} | ||
|
||
return (long)(machineMemory * (maxMemoryPercent / 100.0)); | ||
|
@@ -154,30 +144,33 @@ public static long allowedBytesForMl(long machineMemory, int maxMemoryPercent, b | |
} | ||
|
||
// TODO replace with official ergonomic calculation | ||
private static long dynamicallyCalculateJvmSizeFromNodeSize(long nodeSize) { | ||
if (nodeSize < ByteSizeValue.ofGb(2).getBytes()) { | ||
return (long) (nodeSize * 0.40); | ||
public static long dynamicallyCalculateJvmSizeFromNodeSize(long nodeSize) { | ||
// While the original idea here was to predicate on 2Gb, it has been found that the knot points of | ||
// 2GB and 8GB cause weird issues where the JVM size will "jump the gap" from one to the other when | ||
// considering true tier sizes in elastic cloud. | ||
if (nodeSize < ByteSizeValue.ofMb(1280).getBytes()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be good to add a comment here that the reason this is different to the value used in the JVM heap size formula is that that formula does not have a well-defined inverse, so we pick a value here that makes the inverse pick the correct value for some node sizes we know Cloud uses, in particular 2GB. |
||
return (long)(nodeSize * 0.40); | ||
} | ||
if (nodeSize < ByteSizeValue.ofGb(8).getBytes()) { | ||
return (long) (nodeSize * 0.25); | ||
return (long)(nodeSize * 0.25); | ||
} | ||
return ByteSizeValue.ofGb(2).getBytes(); | ||
return STATIC_JVM_UPPER_THRESHOLD; | ||
} | ||
|
||
private static long dynamicallyCalculateJvmSizeFromNativeMemorySize(long nativeMachineMemory) { | ||
public static long dynamicallyCalculateJvmSizeFromNativeMemorySize(long nativeMachineMemory) { | ||
// See dynamicallyCalculateJvm the following JVM calculations are arithmetic inverses of JVM calculation | ||
// | ||
// Example: For < 2GB node, the JVM is 0.4 * total_node_size. This means, the rest is 0.6 the node size. | ||
// So, the `nativeAndOverhead` is == 0.6 * total_node_size => total_node_size = (nativeAndOverHead / 0.6) | ||
// Consequently jvmSize = (nativeAndOverHead / 0.6)*0.4 = nativeAndOverHead * 2/3 | ||
long nativeAndOverhead = nativeMachineMemory + OS_OVERHEAD; | ||
if (nativeAndOverhead < (ByteSizeValue.ofGb(2).getBytes() * 0.60)) { | ||
return (long) Math.ceil(nativeAndOverhead * (2.0 / 3.0)); | ||
return Math.round((nativeAndOverhead / 0.6) * 0.4); | ||
} | ||
if (nativeAndOverhead < (ByteSizeValue.ofGb(8).getBytes() * 0.75)) { | ||
return (long) Math.ceil(nativeAndOverhead / 3.0); | ||
return Math.round((nativeAndOverhead / 0.75) * 0.25); | ||
} | ||
return ByteSizeValue.ofGb(2).getBytes(); | ||
return STATIC_JVM_UPPER_THRESHOLD; | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that this method is needed suggests there are bugs in the other calculations. It's good to have it as a safety net, but I think it would be good if it could log a warning if it changes the answer by more than the few bytes that could be attributed to rounding errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
100%, I will add a log line.
I do think we need this method as this has been an issue in a couple known scenarios and it can be exceptionally frustrating to troubleshoot.