Created on 2013-05-06 11:06 by neologix, last changed 2022-04-11 14:57 by admin. This issue is now closed.

multiprocessing.cpu_count() implementation should be made available in the os module, where it belongs.

Note that I think it might be interesting to return 1 if the actual value cannot be determined, since that's a sensible default, and likely what the caller will do anyway. This contrasts with the current multiprocessing's implementation which raised NotImplementedError.

If you can't determine the number of CPUs, return a clear "can't determine" value, such as 0 or -1. Returning 1 will hide information, and it's an easy default for the caller to apply if they want to.

I am interested to submit a patch on this. Should I move the implementation to os module and made the multiprocessing one as an alias ? or keep it in both places ? I prefer the idea of returning -1 instead of the current way of raising NotImplementedError in case we can not determine the number of CPU(s).

> I am interested to submit a patch on this. Should I move the implementation to os module and made the multiprocessing one as an alias ? or keep it in both places ? Yes, you should move it, add a corresponding documentation to Doc/modules/os.rst (you can probably reuse the multiprocessing doc), and add a test in Lib/test/test_os.py (you can also probably reuse the multiprocessing test). > I prefer the idea of returning -1 instead of the current way of raising NotImplementedError in case we can not determine the number of CPU(s). Seriously, I don't see what this brings. Since the user can't do anything except using 1 instead, why not do this in the library? I've searched a bit, and other platforms (.e.g Java, Ruby) don't raise an exception, and always return a positive value.

Seriously, return zero, and I can use it as: cpu_count = os.cpu_count() or 1 Why throw away information?

Returning 0 or None sounds better to me than 1 or -1. (I have a preference for None)

I also vote +1 for returning None when the information is unknown. Just write "os.cpu_count() or 1" if you need 1 when the count is unknown ;-)

See also #17444, Trent Nelson wrote an implementation of os.cpu_count().

> I also vote +1 for returning None when the information is unknown. I still don't like it. If a function returns a number of CPU, it should either return an integer >= 1, or raise an exception. None is *not* an integer. And returning an exception is IMO useles, since the user can't do anything with anyway, other than fallback to 1. > Just write "os.cpu_count() or 1" if you need 1 when the count is unknown ;-) os.cpu_count() or 1 is an ugly idiom. > See also #17444, Trent Nelson wrote an implementation of os.cpu_count(). I don't see exactly what this C implementation brings over the one in multiprocessing (which is written in Python)?

> I don't see exactly what this C implementation brings over the one > in multiprocessing (which is written in Python)? multiprocessing.cpu_count() creates a subprocess on BSD and Darwin to get the number of CPU. Calling sysctl() or sysctlnametomib() should be faster and use less memory. On Windows, GetSystemInfo() is called instead of reading an environment variable. I suppose that this function is more reliable. Trent's os.cpu_count() returns -1 if the count cannot be read, multiprocessing.cpu_count() raises NotImplementedError.

Fair enough, I guess we should use it then. We just have to agree on the value to return when the number of CPUs can't be determined ;-)

Returning None sounds reasonable to me. Raising an exception pretty much means that the function should always be called in a try/except (unless you are sure that the code is running on an OS that knows the number of CPUs). Returning -1 is not very Pythonic, and between 0 and None I prefer the latter, since it's IMHO a clearer indication that the value couldn't be determined.

As for why to not return 1, I can imagine code that checks cpu_count, and only if it returns the "don't know" result would it invoke some more expensive method of determining the CPU count on platforms that cpu_count doesn't support. Since the os module is the home for "close to the metal" (well, OS) functions, I agree that it does not make sense to throw away the information that cpu_count can't actually determine the CPU count. Contrawise, I could see the multiprocessing version returning 1, since it is a higher level API and os.cpu_count would be available for those wanting the "don't know" info.
Based on the conversation and the particular inputs to the thread form neologix and ezio, I would like to submit this patch. It probably needs modification(s) as I am not sure what to do with the implementation that is already present in multiprocessing. This patch simply calls the os.cpu_count() from multiprocessing now and behaves as it would have previously. The test cases are also added to test_os similar to ones from multiprocessing.

> Based on the conversation and the particular inputs to the thread form neologix and ezio, I would like to submit this patch. > > It probably needs modification(s) as I am not sure what to do with the implementation that is already present in multiprocessing. This patch simply calls the os.cpu_count() from multiprocessing now and behaves as it would have previously. Thanks, but it would be better to reuse Trent's C implementation instead of multiprocessing's: http://hg.python.org/sandbox/trent/file/dd1c2fd3aa31/Modules/posixmodule.c#l10213

A few small points: Use `num is None` instead of `num == None`. Use `isinstance(cpus, int)` rather than `type(cpus) is int`. And this I think will throw an exception in Python 3: `cpus >= 1 or cpus == None`, because you can't compare None to 1.

I think the idiom `os.cpu_count() or 1` should be mentioned in the documentation an officially recommended. Otherwise people will produce a non-portable code which works on their developer's computers but not on exotic platforms. I have added some other comments on Rietveld.

I agree with Charles-François. An approach using C library functions is far superior to launching external commands.

> I think the idiom `os.cpu_count() or 1` should be mentioned in the documentation an officially recommended. Otherwise people will produce a non-portable code which works on their developer's computers but not on exotic platforms. And I maintain it's an ugly idiom ;-) Since the user can't do anything except falling back to 1, os.cpu_count() should always return a positive number (1 by default). That's AFAICT what all other platforms (Java, Ruby, etc) do, because it makes sense.

> And I maintain it's an ugly idiom ;-) > Since the user can't do anything except falling back to 1, > os.cpu_count() should always return a positive number (1 by default). The user can also raise an error. For example, if I'm writing a benchmark to measure per-core scaling performance, I would like to bail out if I can't calculate the number of cores (rather than report incorrect results).

> Since the user can't do anything except falling back to 1, > os.cpu_count() should always return a positive number (1 by default). In general I agree with you. Actually the os module should contains two functions: cpu_count() which fallbacks to 1 and is_cpu_counting_supported() for rare need. But this looks even more ugly and I choose single function even if in most cases I need use strange idiom.
Appreciate everyone's feedback. I have modified the patch based on further messages in the thread. @Neologix modified posixmodule according to one in Trent's branch and used that for cpu_count(). Kindly suggest improvements/changes if any. @Ned: Thanks for the suggestions. I have applied them wherever applicable. However regarding "And this I think will throw an exception in Python 3: `cpus >= 1 or cpus == None`, because you can't compare None to 1." It does not throw any exceptions as of now.

Yogesh, didn't you notice comments on Rietveld?
@Serhiy Sorry, I missed your comments in the thread. I have made 2 changes and ignored the cpu_count() returning 0, because it returns -1 on failure, else give the number of CPUs. Also the test_os, checks for 0 return if that was to e the case.

Now we have three cpu_count() functions: multiprocessing.cpu_count() raises an exception on failure, posix.cpu_count() returns -1, and os.cpu_count() returns None. It will be easy to get rid of Python wrapper in the os module and return None directly from C code.
Returning None from C code sounds reasonable to me. Anyone else wants to pitch in with suggestions for/against this?

> Returning None from C code sounds reasonable to me. Anyone else wants to pitch in with suggestions for/against this? Go for it ;-)
Modified patch to return None from C code

@Yogesh: if cpus is None, then this will raise an exception in Python 3: `cpus >= 1 or cpus == None` Perhaps you don't have enough test cases yet.
@Ned: if cpus is None, then this will raise an exception in Python 3: `cpus >= 1 or cpus == None` I understand that cpus >= INTEGER will raise an exception and have already modified the condition to remove that kind of check. I was merely stating that equality checks do not raise exception. eg: >>> cpus = None >>> cpus == 1 False >>> cpus == None True >>> Thanks for pointing me out in the right direction to remove those invalid checks and showing the use of proper alternatives at other places in the patch

Not being able to decide for the default value is not a problem: just an optional "default" argument, which is 1 by default (most convinient value), return default on error. os.get_terminal_size() has a similar API for example.
@STINNER: I don't understand. Where exactly should the patch handle this?
Modified patch based on review by neologix

Here is a new patch (cpu_count.patch) with a different approach: * add a os.cpu_count() which returns the raw value (may be zero or negative if the OS returns a dummy value) and raise an OSError on error * os.cpu_count() is not available on all platforms * shutil.cpu_count() is the high level API using 1 as a fallback. The fallback value (which is 1 by default) is configurable, ex: shutil.cpu_count(fallback=None) returns None on os.cpu_count() error. * multiprocessing.cpu_count() simply reuses shutil.cpu_count() So os.cpu_count() as a well defined behaviour, and shutil.cpu_count() is the convinient API. My patch is based on issue17914-4.patch and so also on Trent Nelson's code. I only tested my patch on Linux. It must be tested on other platforms. If nobody tests the patch on HPUX, it would be safer to remove HPUX support. It looks like Trent's code was not tested, I don't think that his code works on platforms other than Windows. test_os will fail if os.cpu_count() fails. The test should be fixed to handle failures, but I prefer to start with a failing test to check if the error case occurs on a buildbot.

The return type of the C function sysconf() is long, but Python uses int: I opened the issue #17964 to track this bug. (os.cpu_count() uses sysconf()).

> Here is a new patch (cpu_count.patch) with a different approach: > > * add a os.cpu_count() which returns the raw value (may be zero or negative if the OS returns a dummy value) and raise an OSError on error > * os.cpu_count() is not available on all platforms > * shutil.cpu_count() is the high level API using 1 as a fallback. The fallback value (which is 1 by default) is configurable, ex: shutil.cpu_count(fallback=None) returns None on os.cpu_count() error. > * multiprocessing.cpu_count() simply reuses shutil.cpu_count() Do we really need cpu_count() in three places with different semantics? Also, it doesn't belong to shutil(), which stands for shell utilities IIRC. IMO just one version in Modules/posixmodule.c is enough (with multiprocessing's version as an alias), there's no need to over-engineer this. It can return None, that's fine with me now at this point. > I only tested my patch on Linux. It must be tested on other platforms. If nobody tests the patch on HPUX, it would be safer to remove HPUX support. Well, HP-UX isn't officially supported, so that's reasonable. > test_os will fail if os.cpu_count() fails. The test should be fixed to handle failures, but I prefer to start with a failing test to check if the error case occurs on a buildbot. That's reasonable.

> * add a os.cpu_count() which returns the raw value (may be zero or > negative if the OS returns a dummy value) and raise an OSError on > error > * os.cpu_count() is not available on all platforms > * shutil.cpu_count() is the high level API using 1 as a fallback. The > fallback value (which is 1 by default) is configurable, ex: > shutil.cpu_count(fallback=None) returns None on os.cpu_count() error. -1. This is simply too complicated for a simple API. Just let os.cpu_count() return None.
@Stinner: 1. While I agree with your idea of what you have done in test_os, (particularly, for determining if platform is supported or not) there seems to be no reason(AFAIK) to have a shutil for cpu_count. I agree with neologox there. 2. Also I am not comfortable with the idea of having multiple 'implementations' of cpu_count. "There should be one-- and preferably only one --obvious way to do it." 3. The idea of returning 1 by default does not seem to serve a useful purpose. It should be left to the end-user to decide what needs to be done based on error/actual_value received from system. (+1 to Antoine and nedbat) For eg, a. Let's say someone works on scheduling and power managment modules. It is important to know that the platform does not support providing cpu_count() instead of giving 1. This will ensure that they don't go about erroneously setting wrong options for scheduler and/or overclocking the CPU too much(or too little). b. On the other hand if another user just wants to use a cpu_count number from a his application to determine the number of threads to spawn he can set th = cpu_count() or 1 (on a side note: *usually* for programs that are non IO intensive and require no/little synchronization it is best to spawn cpu_count() number of threads) These are just 2 examples to demonstrate that it must be the end-user who decides what to do with the proper_value or reasonable_error_value given by cpu_count() 4. +1 to Antoine on last comment ;-)
Modified patch based on further comments and review. 1. Removed *everything* from os.py 2. removed typecasting for ncpu where not required. 3. removed redundant comments

Just for giggles, here's the glibc default implementation on non Linux platforms: http://sourceware.org/git/?p=glibc.git;a=blob;f=misc/getsysstats.c;hb=HEAD """ int __get_nprocs () { /* We don't know how to determine the number. Simply return always 1. */ return 1; } """ And on Linux, 1 is returned as a fallback when you don't have the right /sys or /proc entry: http://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/getsysstats.c (The enum discussion enlighted me, endless discussions are so fun!)

> And on Linux, 1 is returned as a fallback when you don't have the > right /sys or /proc entry: > http://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/getsysstats.c > > (The enum discussion enlighted me, endless discussions are so fun!) Do you want cpu_count() to return an enum? >>> os.cpu_count() <CPUCount.Four: 4>

Python's goal is not to emulate the suboptimal parts of other languages. We have dynamic typing, and so can return None from the same function that returns 1. And we have compact expressions like `cpu_count() or 1`, so we don't have to make unfortunate compromises.

> Python's goal is not to emulate the suboptimal parts of other languages. Well, I'm sure they could have returned -1 or 0, which are valid C long distinct from any valid integer representing a number of CPUs. If the libc guys (and many other APIs out there ), chose to return 1 as default, there's a reason. Furthermore, you're missing the point: since the underlying libraries os.cpu_count() rely on return 1 when they can't determine the number of CPUs, why complicate the API by pretending to return None in that case, since you can't detect it in the first place? > We have dynamic typing, and so can return None from the same function that returns 1. And we have compact expressions like `cpu_count() or 1`, so we don't have to make unfortunate compromises. That's not because it's feasible that it's a good idea. Dynamic typing is different from no typing: the return type of a function is part of its API. You can't return None when you're supposed to return an int. If I go to a coffee machine to get some chocolate and there's no chocolate left, I don't want it to return me a coffee instead. What's looks more natural if os.cpu_count() is None or if os.cpu_count() >= 1 Even in dynamic typing, it's always a good thing to be consistent in parameter and return value type. Why? For example, PEP 362 formalizes function signatures. With a os.cpu_count() returning a number (or eventually raising an exception), the signature is: def cpu_count() -> int [...] What does it become if you can return None instead? For example, there's some discussion to use such signatures or other DSL to automatically generate the glue code needed to parse arguments and return values from C extension modules (PEP 436 and 437). Basically, you just implement: /* ** [annotation] ** @return int */ long cpu_count_impl(void) { long result = 1; #ifdef _SC_NPROCESSORS_CONF long = sysconf(_SC_NPROCESSORS_ONL); [...] #fi return result; } And the DSL processor takes care of the rest. What does this become if your return object isn't typed? Really, typing is of paramount importance, even in a dynamically typed language. And pretending to return a distinct value is pretty much useless, since the underlying platform will always return a default value of 1. Plus `cpu_count() or 1` is really ugly. Now I hope I made my point, but honestly at this point I don't care anymore, since in practice it should never return None. Documenting the None return value is just noise in the API...

> > Python's goal is not to emulate the suboptimal parts of other languages. > > Well, I'm sure they could have returned -1 or 0, which are valid C > long distinct from any valid integer representing a number of CPUs. If > the libc guys (and many other APIs out there ), chose to return 1 as > default, there's a reason. Well, they can be wrong sometimes, too :-) > Furthermore, you're missing the point: since the underlying libraries > os.cpu_count() rely on return 1 when they can't determine the number > of CPUs, why complicate the API by pretending to return None in that > case, since you can't detect it in the first place? The patch doesn't seem to rely on the glibc, so we are fine here. Or do the other libs work likewise? > And the DSL processor takes care of the rest. > > What does this become if your return object isn't typed? It's typed, just the type is "int or None". I'm sure some statically-typed languages are able to express this (OCaml? Haskell?). Anyway, I don't mind whether it's None or 0 or -42. But let's not hide the information.
Based on the last 3 messages by Ned, Charles and Antoine, I keep thinking that arguments made by Charles are very valid ones and that it would be better to return 1. I say this (partly from the 'type' argument, but), mainly, *if* its known that the underlying libraries are returning 1 on failure. *If* that is the case I see no reason try to return None (which, btw, will never happen if none of the calls return non-positive values ever).

> Well, they can be wrong sometimes, too :-) Indeed, as can I ;-) > The patch doesn't seem to rely on the glibc, so we are fine here. > Or do the other libs work likewise? sysconf(_SC_NPROCESSORS_CONF) is implemented with the above function in the glibc. For other Unix systems apparently they use the sysctl() syscall, and I don't *think* it can fail to return the number of CPUs. So the only platforms where it could in theory fail are things like Cygwin, etc. > >> And the DSL processor takes care of the rest. >> >> What does this become if your return object isn't typed? > > It's typed, just the type is "int or None". I'm sure some > statically-typed languages are able to express this (OCaml? Haskell?). I recently started learning Haskell. You have Either and Maybe that could fall into that category. > Anyway, I don't mind whether it's None or 0 or -42. But let's not hide > the information. I liked your suggestion of making it an enum: >>> os.cpu_count() <CPUCount.UnknownCount: 42> Nah, None is fine to me!
Minor modifications based on review comments. 1. Change mib array size to 2, 2. return value set to 0 consistently (in C code), and 3. removed IRIX #defines
Typo fix

+1 for returning None. I haven't looked into patches but if needed feel free to borrow some code from psutil: Linux: https://code.google.com/p/psutil/source/browse/psutil/_pslinux.py?spec=svn30f3c67322f99ab30ed87205245dc8394f89f0ac&r=c970f35bc9640ac32eb9f09de8c230e7f86a2466#44 BSD / OSX: https://code.google.com/p/psutil/source/browse/psutil/_psutil_bsd.c?spec=svn30f3c67322f99ab30ed87205245dc8394f89f0ac&r=9b6e780ea6b598a785670c2626c7557f9fef9238#486 Windows: https://code.google.com/p/psutil/source/browse/psutil/_psutil_mswindows.c?spec=svn30f3c67322f99ab30ed87205245dc8394f89f0ac&r=4d5b0de27024e9d3cd6a3573a493290498afa9c2#426 SunOS: https://code.google.com/p/psutil/source/browse/psutil/_pssunos.py?spec=svnff76a4e33da359162c28f8c7478f9e6c6dff347b&name=sunos&r=d53e11edfbe18d22f4e08168f72b1952cfaef373#27

New changeset 5e0c56557390 by Charles-Francois Natali in branch 'default': Issue #17914: Add os.cpu_count(). Patch by Yogesh Chaudhari, based on an http://hg.python.org/cpython/rev/5e0c56557390

Alright, committed. Yogesh, thanks for the patch! I'm attaching a patch to replace several occurrences of multiprocessing.cpu_count() by os.cpu_count() in the stdlib/test suite.

In my patch cpu_count.patch, I changed posix_cpu_count(): * rewrite Mac OS X implementation: code in 5e0c56557390 looks wrong. It gets a MIB but then don't use it when calling _bsd_cpu_count(). But I didn't check my patch nor the commit version on Mac OS X. * use "int ncpu;" instead of "long ncpu;" when calling mpctl() and sysctl(). For mpctl(), ncpu is used to store the result, so a wide type is ok. But for sysctl(), we pass a pointer. What happens if sysctl() expects int whereas we pass a pointer to a long? We announce sizeof(int)!? * inline _bsd_cpu_count() Sorry for this late review.

New changeset a85ac58e9eaf by Charles-Francois Natali in branch 'default': Issue #17914: Remove OS-X special-case, and use the correct int type. http://hg.python.org/cpython/rev/a85ac58e9eaf

New changeset f9d815522cdb by Charles-Francois Natali in branch 'default': Issue #17914: We can now inline _bsd_cpu_count(). http://hg.python.org/cpython/rev/f9d815522cdb

> * rewrite Mac OS X implementation: code in 5e0c56557390 looks wrong. It > gets a MIB but then don't use it when calling _bsd_cpu_count(). But I didn't > check my patch nor the commit version on Mac OS X. Indeed. I just removed the OS-X special case altogether. Apparently, the standard sysctl is supposed to work os OS-X. We'll see what happens. > * use "int ncpu;" instead of "long ncpu;" when calling mpctl() and > sysctl(). For mpctl(), ncpu is used to store the result, so a wide type is > ok. But for sysctl(), we pass a pointer. What happens if sysctl() expects > int whereas we pass a pointer to a long? We announce sizeof(int)!? Ouch. This was overlooked when the type was changed to long. I fixed this. > * inline _bsd_cpu_count() Done in a subsequent commit (especially since the OS-X special case has been removed).

New changeset 6a0437adafbd by Charles-François Natali in branch 'default': Issue #17914: Use os.cpu_count() instead of multiprocessing.cpu_count() where http://hg.python.org/cpython/rev/6a0437adafbd
In message http://bugs.python.org/issue17914#msg188626 Victor Stenner says "On Windows, GetSystemInfo() is called instead of reading an environment variable. I suppose that this function is more reliable." From my reading, and based on feedback from one of my customers, I believe he is correct and that GetSystemInfo() ought to be used on Windows. (It is available in pywin32 win32api.)

> From my reading, and based on feedback from one of my customers, I believe he is correct and that GetSystemInfo() ought to be used on Windows. (It is available in pywin32 win32api.) Please open a new issue to suggest this enhancement, this issue is closed.
Since this is closed I've created a new issue as requested: http://bugs.python.org/issue23037