Different manifestations of the same logic yield very different execution times
Since you are not allowed to share any more information on the queries such as sample data + table definition, these tests will be on own fabricated data and with or without indexes. Test data below.

YMMV.

The inner join + group by query

The 'bad' query puts everything in an index spool, and subsequentially executes a nested loop join on the exponential 8M rows



Whole plan



Index Spool properties.

This index spool creates over 8M records as a result of the &lt;= join. 
The nested loop operator over 8M rows into a stream aggregate will also decrease performance.

The derived table

The 'better' plan



The stream aggregate is used before the nested loop operator is used. Resulting in a much smaller join.
There is also a difference in stream agg operators, this one supports the MAX() function while the other one (above) supports the GROUP BY statement + The partialagg of MAX().

TL;DR

My guess is that the huge nested loops operator used in the plan of the inner join query factors in for the excessive time. 
The difference with the good plan being that the Stream aggregate happens before the nested loops operator.
Also as @ypercubeᵀᴹ mentioned, they are not logically similar.



Adding indexes

CREATE INDEX IX_widgets_population_serial_ID on widgets_population(serial_ID);
CREATE INDEX IX_widgets_tested_serial_ID on widgets_tested(serial_ID);


The 'good' query gets improves a lot after an index, using 15ms cpu time



The reason that this change is so drastic, is that the TOP operator is used now.



This top operator is getting the first value that matches the &lt;= condition by using the index's sort order, for each value in widgets_tested.

The 'bad' query, not so much



Executing the join before executing GROUP BY &amp; MAX(), where the MAX() is calculated before the join in the good plan.



Data used for this example

CREATE TABLE  widgets_population(serial_ID int)

CREATE TABLE  widgets_tested(serial_ID int)

SET NOCOUNT ON
DECLARE @I INT = 1;
WHILE @I  &lt;= 1000
BEGIN
INSERT INTO  widgets_population(serial_ID)
VALUES(@I)
INSERT INTO  widgets_tested(serial_ID)
VALUES(@I)
SET @I+=1
END

INSERT INTO  widgets_population(serial_ID)
select serial_ID+1000 from widgets_population

INSERT INTO  widgets_tested(serial_ID)
select serial_ID+1000 from widgets_tested

INSERT INTO  widgets_population(serial_ID)
select serial_ID+2000 from widgets_population

INSERT INTO  widgets_tested(serial_ID)
select serial_ID+2000 from widgets_tested