镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix: qrandint/qlograndint never sample the documented inclusive upper bound - #1606

Merged
Li Jiang (thinkall) merged 13 commits into
microsoft:mainfrom
AmirF194:fix/qrandint-inclusive-upper-bound
Sep 18, 2026
Merged

Li Jiang (thinkall) merged 13 commits into
microsoft:mainfrom
AmirF194:fix/qrandint-inclusive-upper-bound

Conversation

@AmirF194

Copy link
Copy Markdown
Contributor

Fixes #1605

Quantized.sample()'s q=1 fast path bypasses quantization and calls the wrapped Integer sampler directly, which draws exclusive of domain.upper (matching plain randint's own exclusive-upper contract). Since qrandint's default is q=1, its documented inclusive upper bound was unreachable. For q>1 the general branch rounds a raw exclusive-upper draw, so whether the nominal upper is reached depends on the rounding fraction, e.g. qrandint(0, 10, 2) topped out at 8.

Fixed by, for Integer domains only, extending the sampled range by one quantization step before drawing and clamping the result back to the true upper bound afterwards. Plain randint/lograndint never go through Quantized at all, so they're untouched, and I left the Float q=1 fast path alone since #1599 is already narrowing that one.

One thing to flag: this touches the same if self.q == 1: line as #1599. Whichever lands first, the other will need a small rebase.

Added tests for qrandint at q=1 (default and explicit) and q=2, and qlograndint at q=1: none of them can reach the upper bound on main, all do here. Also a control test that plain randint keeps its exclusive upper. test/tune/test_constraints.py exercises qrandint inside a real BlendSearch run and still passes.

… bound

Quantized.sample()'s q=1 fast path skips quantization and calls the wrapped
Integer sampler directly, which draws exclusive of domain.upper, same as
plain randint. Since qrandint defaults to q=1, its own "upper is also
inclusive (!)" docstring was never true. For q>1 the general branch rounds
a raw exclusive-upper draw, so reaching the nominal upper depends on the
rounding fraction.

Fixed by extending the sampled range by one quantization step for Integer
domains before drawing, then clamping back to the true upper bound. Plain
randint/lograndint never go through Quantized, so they are untouched.

Fixes microsoft#1605

@thinkall Li Jiang (thinkall) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two correctness issues remain:

  1. flaml/tune/sample.py:459: routing the default q=1 path through float quantization changes batched samples from integer NumPy arrays to lists of np.float64, and can lose precision above 2**53. Preserve an integer-only q == 1 path and add scalar/batched type and large-bound tests.
  2. flaml/tune/sample.py:470-474: extending the sampler by q draws values outside the requested domain and clamps them into the upper endpoint, biasing its probability (also in log space). Extend an integer sampler's exclusive upper bound by one, or sample quantization-grid indices directly, and add deterministic edge-bin/distribution tests.

The q=1 fast path for Integer domains routed through float division,
turning integer samples into np.float64 and losing precision above
2**53. For q>1, extending the raw draw's range by a full q and
clamping the overshoot into the top bin gave that bin roughly 1.4x
the width of an interior bin (measured with a fixed seed: 40164 vs
~39800 draws per bin over 200000 samples on a 5-bin grid, expected
40000 each).

Draw the grid point's index directly instead: every point, including
the top one, is reachable through exactly one raw index, so no bin
is over- or under-represented and the result stays a plain int with
no rounding.

Added scalar/batched dtype tests, a large-bound precision test past
2**53, and a fixed-seed bin distribution test that would have caught
the top-bin bias.
@AmirF194

Copy link
Copy Markdown
Contributor Author

Both real. Pushed fd63fcd: instead of rounding a raw draw and clamping, it now draws the quantization grid point's index directly, so the q=1 default stays a plain int (no float division) and every grid point including the top one gets exactly one raw index instead of the top bin absorbing the clamped overshoot.

Verified the bias with a fixed seed: the old code put 40164 of 200000 draws in the top bin of a 5-bin grid against ~39800 for the others, expected 40000 each. New code comes back within 0.5% on every bin. Added that as a deterministic test, plus scalar/batched dtype checks and a large-bound (2**60) precision test. Ran the full test/tune/test_sample.py suite locally, 9 passed.

@thinkall Li Jiang (thinkall) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit fixes the previous out-of-domain endpoint clamping, but three blockers remain:

  1. flaml/tune/sample.py:469-478: computing grid bounds with domain.lower / self.q converts integers to float. For nonzero bounds above 2**53, scalar sampling can return a value below the requested domain and batched sampling can overflow on Windows. Calculate ceiling/floor grid indices using integer division and keep offset arithmetic in Python/safe integer types; add scalar and batched large nonzero-bound tests.
  2. flaml/tune/sample.py:478: the q=1 batched compatibility issue remains. Historically size > 1 returned an integer NumPy array; the new path returns a Python list. Preserve the ndarray shape/dtype and assert the container and dtype in the regression test.
  3. flaml/tune/sample.py:472-478: rebasing the grid to indices starting at 1 changes qlograndint probabilities whenever lower > q. For example, qlograndint(10, 20, 1) samples log-uniformly over 1..11 and shifts by nine instead of sampling over the actual 10..20 values. Sample absolute grid indices (or weight the actual grid values) and add a deterministic distribution test with lower > q.

…nteger arithmetic

Addresses thinkall's second review round on microsoft#1606: qlograndint drew log-
uniformly over an index range rebased to start at 1, which is a different,
shifted distribution from the actual value grid whenever lower > q (log-
uniform is scale-invariant under multiplication, not under an additive
shift). Switched to the grid's own index (value // q). Also replaced the
remaining float division for computing grid bounds with integer ceiling/
floor division, and the batched Integer path now returns an int64 ndarray
instead of a Python list, matching the historical container and dtype.
@AmirF194

Copy link
Copy Markdown
Contributor Author

Pushed b7d9bfb.

1/2: replaced the float division with integer ceiling/floor division for the grid bounds, and the batched path now returns an int64 ndarray (matches test_qrandint_returns_plain_int_scalar_and_batched and a new test_qrandint_large_nonzero_lower_bound_batched).

3: sampled the grid's own index (value // q) instead of one rebased to start at 1. LogUniform is scale-invariant under multiplication but not under an additive shift, so the rebase was the actual bug. Added test_qlograndint_stays_log_uniform_when_lower_much_greater_than_q, which compares the empirical mean of qlograndint(1000, 1010, 1) against the analytic LogUniform(1000, 1010) mean. Checked it discriminates: reverting just the sample.py change fails that test with a 2.0 gap against a 0.1 tolerance (the rebased implementation's mean sits about 200 standard errors off).

Full suite (11 tests) passes in the repo's own Docker command.

@thinkall Li Jiang (thinkall) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest commit resolves the large-integer precision issue, restores the q=1 batched ndarray contract, and samples qlograndint from absolute grid values. One compatibility blocker remains:

  • flaml/tune/sample.py:484: the unconditional ndarray return also changes batched qrandint/qlograndint with q > 1 from their historical Python list return to np.ndarray. Preserve the exact integer-grid calculation, but return an ndarray only for q == 1 and retain the list container for q > 1. Update test/tune/test_sample.py:100-102 to assert the two distinct historical contracts rather than codifying the broader return-type change.

…ive-upper-bound

# Conflicts:
#	flaml/tune/sample.py
@AmirF194

Copy link
Copy Markdown
Contributor Author

Rebased onto main, it conflicted with #1599 on the same line. Both PRs were carving out opposite exceptions to the same q==1 fast path (yours excluded Float, mine excluded Integer), so the correct merge removes the fast path for both types and always routes through quantization. Pushed 43f1799; all 29 tests pass locally including your new test_unit_quantization.py.

@thinkall Li Jiang (thinkall) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main-branch integrations preserve the inclusive-bound, integer-precision, and log-space fixes, but the remaining compatibility blocker is unchanged:

  • flaml/tune/sample.py:481: batched integer quantization unconditionally returns np.ndarray. Historically q == 1 returned an integer ndarray, while q > 1 returned a Python list through list(quantized). Preserve the ndarray only for q == 1, retain the list contract for q > 1, and update test/tune/test_sample.py:101 so it does not codify the regression.

Quantized.sample()'s Integer branch unconditionally returned an ndarray
for size > 1, but only q == 1 historically did; q > 1 returned a plain
Python list (matching the float quantization path's list(quantized)).
Return a list for q > 1 and keep the ndarray only for q == 1.

Updated the two tests that asserted ndarray for a q > 1 domain so they
check the correct historical container instead.
@AmirF194

Copy link
Copy Markdown
Contributor Author

Right, that unification was the actual gap. Pushed 8c4a8c0: q == 1 still returns the ndarray, q > 1 now returns a plain list via list(values), and the two tests that had assumed ndarray for a q > 1 domain now check the list container instead.

@thinkall Li Jiang (thinkall) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The q > 1 list contract is restored, but one q == 1 compatibility issue remains:

  • flaml/tune/sample.py:476-486: converting indices to np.int64 before the q == 1 branch changes the wrapped sampler's historical ndarray dtype. On Windows, the prior qrandint(..., size>1) path returned int32; the current head forces int64. Return indices directly for batched q == 1, and perform the int64 conversion/multiplication only for the q > 1 list path. Add a regression assertion matching the underlying sampler's dtype.

@AmirF194

Copy link
Copy Markdown
Contributor Author

Right, forcing int64 there was unnecessary. Pushed 2ecd58a: q == 1 now returns indices unchanged (no dtype conversion), and the int64 cast plus multiply only happens on the q > 1 list path. Added a test that swaps in a fake int32 sampler and asserts the returned array keeps that dtype; it fails against the previous head.

@thinkall Li Jiang (thinkall) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous q == 1 dtype blocker is resolved: returning indices preserves the wrapped sampler's platform-specific integer ndarray dtype. One large-domain blocker remains:

  • flaml/tune/sample.py:487: batched q > 1 converts indices to np.int64 before multiplication. For grids above 2**63 - 1, multiplication wraps and can return zero or negative values outside the requested domain, while scalar sampling remains correct. Construct the historical list by multiplying each index as a Python int, and extend the large batched qrandint/qlograndint tests beyond signed 64-bit range.

Grid points above 2**63 - 1 wrapped silently through numpy int64
multiplication in the batched q > 1 Integer path; multiply each index
as a Python int instead.
@AmirF194

Copy link
Copy Markdown
Contributor Author

Batched q > 1 now multiplies each grid index as a Python int instead of casting through np.int64, so it no longer wraps above 263 - 1. Added test_qrandint_batched_beyond_int64_range (q=262, upper=2**65); against the prior code it fails with a negative wrapped value, e.g. -4611686018427387904. Full 13-test suite plus ruff check pass in the repo's own Docker verify command. Pushed 6142c34.

@thinkall Li Jiang (thinkall) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the changes since my last review. Batched q > 1 multiplication now stays in Python integers, preventing signed-64-bit overflow while preserving the historical contracts: scalar Python integers, q == 1 ndarray with the underlying platform dtype, and q > 1 Python lists. Inclusive bounds, negative/non-divisible domains, large-integer precision, and actual-grid qlograndint behavior are covered and pass. No blocking issues found.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The implementation changes the public custom-sampler contract for integer quantized domains and can produce incorrect results.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Fixes inclusive upper-bound sampling for quantized integer samplers while preserving plain integer sampler behavior.

Changes:

  • Updates integer quantized sampling and upper-bound handling.
  • Adds regression coverage for bounds, precision, batching, and distributions.
  • Preserves float quantization behavior.
File summaries
File Summary
test/tune/test_sample.py Adds comprehensive regression tests.
flaml/tune/sample.py Updates integer quantized sampling.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread flaml/tune/sample.py Resolved

@jianglibigdata jianglibigdata left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff doesn't look right. All the existing code is removed and the actual diff is not clear.

Comment thread flaml/tune/sample.py Resolved
The prior merge from main flattened this file's CRLF line endings to
LF, so the PR diff showed the whole file as removed and re-added
instead of the actual ~30-line change.
@AmirF194

Copy link
Copy Markdown
Contributor Author

Good catch, that was a real problem. A merge from main earlier in this branch's history flattened this file's line endings from CRLF to LF, so the diff showed the whole file as rewritten. Converted it back to CRLF and pushed; the diff is now the actual ~30-line change plus the new test file.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Correct the biased log-uniform expected-value assertion in test/tune/test_sample.py.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread test/tune/test_sample.py Resolved

@thinkall Li Jiang (thinkall) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the updates since my prior approval. The author commit changes only line endings, and the merge from main does not touch the sampling implementation or tests. The approved scalar/container/dtype contracts, large-integer Python arithmetic, inclusive bounds, precision, and log-grid behavior remain intact. No blocking issues found.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Non-divisible upper bounds remain unreachable despite the documented inclusive contract.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

test/tune/test_sample.py:208

  • This repeats both domain construction and scalar log-sampling 200,000 times. A single batched sample exercises the same distribution while avoiding hundreds of thousands of Python calls and object copies.
    samples = [qlograndint(lower, upper, q).sample(spec=None, random_state=rs) for _ in range(n)]
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread flaml/tune/sample.py Resolved
Comment thread test/tune/test_sample.py Resolved
Comment thread flaml/tune/sample.py Resolved
…lt-in samplers

A custom sampler on an Integer domain got the grid-index domain (and,
for q > 1, its output multiplied by q) unconditionally, which breaks
any sampler that reads domain.lower/domain.upper or returns actual
values rather than an index. Only Integer._Uniform and
Integer._LogUniform use the grid transform now; every other sampler
keeps the historical actual-value contract, q == 1 calling it directly
and q > 1 falling through to the existing float-based quantization.
@AmirF194

Copy link
Copy Markdown
Contributor Author

Applied the suggested fix as given: uses_integer_grid gated on exact
sampler type, q == 1 calls the sampler directly on the real domain,
q > 1 falls through to the existing float quantization. Pushed
6320d97.

Added the two regression tests: a custom subclass of Integer._Uniform
(exact-type check treats it as non-built-in on purpose) that records
the domain it's called with, one for q == 1 and one for q == 2. Both
fail against the pre-fix code with the sampler seeing the grid-index
domain (0, 21) instead of the real (0, 20). Full suite is 15 passed,
pre-commit clean.

The custom-sampler-contract fix (6320d97) widened the q == 1 direct
passthrough from Integer-only to isinstance(domain, Integer) == False
domains too, so a Float domain (quniform/qloguniform/qrandn) at q == 1
skipped the round-to-nearest-unit step entirely and returned the raw
unrounded sample. Scope the passthrough back to Integer domains, which
is what it was gating before this round's change.

test/tune/test_unit_quantization.py::test_unit_quantization_rounds_float_samples
covers this; it fails on the current head (6 of 18 parametrizations,
all q=1) and passes with this fix.
@AmirF194

Copy link
Copy Markdown
Contributor Author

CI caught a regression from the last push: the q == 1 passthrough I widened to gate on uses_integer_grid also matched Float domains, so quniform/qloguniform/qrandn at q == 1 stopped rounding to the nearest unit (test_unit_quantization.py, 6 of its 18 cases). Scoped the passthrough back to Integer domains only, which is what it gated before this round.

Verified: reverted only sample.py and reran the failing parametrizations, all 6 fail with the exact unrounded-vs-rounded mismatch; with the fix, the full test_sample.py + test_unit_quantization.py suite (33 tests) passes in a clean container. Pushed c095728.

@thinkall
Li Jiang (thinkall) added this pull request to the merge queue Sep 18, 2026
Merged via the queue into microsoft:main with commit fbf104e Sep 18, 2026
16 checks passed
@AmirF194

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review rounds on this one, Li Jiang (@thinkall). The q==1/q>1 contract split was the part I kept getting wrong, and your catches on the int64 overflow and CRLF diff noise both landed real bugs.

@AmirF194
Amir Fathi (AmirF194) deleted the fix/qrandint-inclusive-upper-bound branch September 18, 2026 09:21
Li Jiang (thinkall) added a commit that referenced this pull request Sep 28, 2026
## Summary
Merge current public main, retaining the collaborator's quantized-sampling
update, the ordinal-encoder refactor, and the current Codecov action.

Port the missing internal fANOVA replay expansion and fitted-model forecast
frequency fix. Bring over notebook artifact isolation and related ensemble
and resampler regression coverage without merging internal Git history.

## Prompting Intent
The engineer requested another comparison and synchronization after new
FLAML-Internal PRs merged. Keep GitHub as the maintained library source,
preserve public installation and forecasting contracts, and exclude internal
delivery files. After discussing smoothing initialization, the engineer
requested the better general choice everywhere rather than Fabric-specific
initialization behavior.

## Linked Sources
- Consolidation PR: #1545
- Current public main: 16bc42e
- Public categorical encoding update: #1569
- Public sampling update already on this branch: #1606
- Internal changes audited from the newly merged source; provenance remains in the internal repository
- Runtime and synchronization decisions: flaml/fabric/README.md

## Rationale
Widen replay distributions around observed trials without mutating the original
search space or discarding compatible log scales and grids. Derive a regular
forecast calendar from the model actually fitted to the CV fold, not from the
full dataset's irregular-date fallback.

Retain estimated smoothing initialization uniformly: it fits the initial state
instead of locking it to the first observation. Keep the existing public
DatetimeIndex/date-gap contract and package version, rather than importing
internal observation-row output or release-only metadata. Changes already
present in the public branch are not duplicated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b0e4942d-f489-4b42-8a90-097ede4e88f7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

qrandint/qlograndint's documented inclusive upper bound is never sampled

4 participants